Android Architecture Components + Kotlin Croutine

Androidでasync awaitする時のライフサイクルとのバインド 初ブログで慣れませんが頑張ります。

ごめんなさいですが、ここではAndroid Architecture Componentsについての詳しい解説はしないです…
Android Arch(以下略)については公式のドキュメントを見ると良いと思います。
Android Architecture Components

例としてここでは画像のダウンロードと表示を行なうようなプログラムを書いていきます。
(画像のダウンロード処理の実装は省きます)
前提として画像はActivity起動時の一度しかダウンロードせず、さらにそれはキャッシュされるという条件で作っていきます。


Android Arch(以下略)のViewModelとLiveDataを使用しているならば、以下のようにすることでそれは実装できます。

//とりあえずこんな感じの拡張関数を用意
//簡単にLiveDataをmapできるようにして(Listのmapとかと大体同じ感じ)
fun <T, R> LiveData<T>.map(func: (T) -> R): LiveData<R> = Transformations.map(this, func)
fun <T> MutableLiveData<T>.mediate() = map { it }

画像を表示したいActivityで使うViewModelを定義

//ImageRepositoryという画像のDLとキャッシュを行なうためのクラスが外からDIされるということにします
class HogeViewModel @Inject constructor(private val mImageRepository: ImageRepository)
    : ViewModel() {    
    //これの値をViewModel内で弄る
    private val mImageLiveData = MutableLiveData<Bitmap>()

    //mImageLiveDataの変更を仲介して伝播する為のLiveDataを外に公開
    val imageLiveData = mImageLiveData.mediate()
    
    //画像のダウンロードを開始して結果をimageLiveDataまで伝えるメソッド
    fun loadImage(url: String) {
         AsyncTask.execute {
            val bitmap = mImageRepository.downloadOrGet(url)//画像をDLしてキャッシュする処理
            mImageLiveData.postValue(bitmap)//非同期スレッドから値を変更するときはpostValue
        }
    }
}

画像を表示したいActivity

class HogeActivity : AppCompatActivity() {

    //ViewModelに何かしらをDIしたい場合このコードでは無理ですが、例なのであしからず。
    //生成されたActivityのライフサイクルとバインドされたViewModelが取ってこれます。
    //つまり画面回転等でActivityが再生成されたとしても同一のViewModelを持ってきます。
    val model by lazy { ViewModelProviders.of(this).get(HogeViewModel::class.java) }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_hoge)

        val imageView = findViewById<ImageView>(R.id.image)
        model.imageLiveData.observe(this, Observer {
            imageView.setImageBitmap(it)
        })

        //最後の変更がobserveした時に渡ってくるのでloadImageするのは一回でおk
        if (savedInstanceState == null)
            model.loadImage("hogehogeImageUrl")
    }
}

雑な実装でエラー処理だとか色々と省いてますが、ざっとこんな感じにすれば非同期的に画像をダウンロードしてそれを表示することができるとおもいます。
(適当に書いたのでできなかったらごめんなさい。)

しかし、ModelViewには「外に公開する用のLiveData」と「公開せずに値の変更を行なうためのLiveData」、 Activityでは「observe」と「画像のダウンロードを開始するためのメソッド呼び出し」と画像一回ダウンロードして表示するためだけに少々大仰になってしまいました。
更に画像をダウンロードするために使用している「ImageRepositoryのdownloadOrGet」メソッドで画像のキャッシュも行っているとなればLiveDataを使用する必要性は薄いです。
(LiveDataは最後の変更をobserve時に渡してきます。これにより結果的に画面回転時の復帰が可能です。)

そこでKotlinのCoroutineを用いてできるだけ簡潔になるようにしてみます。


class HogeViewModel @Inject constructor(private val mImageRepository: IImageRepository)
    : ViewModel() {
    //Deferred<Bitmap>が返ります。
    //これをActivity側でawait()します。
    fun loadImage(url: String) = async { mImageRepository.downloadOrGet(url) }
}
class HogeActivity : AppCompatActivity() {

    val model by lazy { ViewModelProviders.of(this).get(HogeViewModel::class.java) }

    private var mLoadImageJob: Job? = null

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_hoge)
        val imageView = findViewById<ImageView>(R.id.image)
        
        //Jobを保存
        mLoadImageJob = launch(UI) {
            val image = model.loadImage("hogehogeImageUrl").await()
            imageView.setImageBitmap(image)
        }
    }

    override fun onDestroy() {
        mLoadImageJob?.cancel()//Activityが死んだら処理をキャンセル
        super.onDestroy()
    }
}

簡潔になりましたが、LiveDataを用いていたときと違いって、onDestroy()でJobの中断処理をしないといけなくなったので何やら辛い感じになってしまいました。
この例では画像一枚を落とすだけなのでこれで済んでいますが、複数のJobを扱うようになると、Jobを生成する度にListに加え、Jobが終わればListから外す、等の処理を書かなければならなくなりますし大変面倒ですし、書き忘れ等があればとても悲しいです。

ですが、幸いAndroid Arch(以下略)には、ライフサイクルを扱うためのモジュールがあるので、これを使えばなんとかなりそうです。 試してみましょう。


Jobとライフサイクルをバインドする関数

//与えられたLifecycleOwnerのライフサイクルのonDestroyで自動的にキャンセルされるJobを作る
fun bindLaunch(owner: LifecycleOwner, start: CoroutineStart = CoroutineStart.DEFAULT,
               block: suspend CoroutineScope.() -> Unit) = launch(UI, start, block).apply {
    val observer = createLifecycleObserver(this)
    val lifecycle = owner.lifecycle
    lifecycle.addObserver(observer)
    //ジョブが終わればLifecycleOwnerからObserverをremove
    invokeOnCompletion { lifecycle.removeObserver(observer) }
}

//与えられたjobを元にLifecycleObserverを生成
private fun createLifecycleObserver(job: Job) = object : LifecycleObserver {
    val mRef = WeakReference<Job>(job)
    
    //onDestroyにバインドされるメソッド
    @OnLifecycleEvent(Lifecycle.Event.ON_DESTROY)
    fun onDestroy() {
        val j = mRef.get() ?: return
        if (!j.isCompleted)  job.cancel()//Jobをキャンセル
    }
}

LifecycleOwnerからニョキッと生やす

//上で定義したbindLaunchのLifecycleOwnerの拡張関数版を定義
//AppCompatActivityはLifecycleOwnerを継承しているのでいい感じに呼び出せます。
fun LifecycleOwner.bindLaunch(start: CoroutineStart = CoroutineStart.DEFAULT,
                              block: suspend CoroutineScope.() -> Unit)
        = bindLaunch(this, start, block)

こんな感じで呼び出すだけでLifecycleOwnerとJobをバインドしてくれる関数を定義してやれば……

class HogeActivity : AppCompatActivity() {
    val model by lazy { ViewModelProviders.of(this).get(HogeViewModel::class.java) }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_hoge)
        val imageView = findViewById<ImageView>(R.id.image)
         bindLaunch {
            val image = model.loadImage("hogehogeImageUrl").await()
            imageView.setImageBitmap(image)
        }
    }
}

ライフサイクルについてあまり考えずにasync/awaitをActivity上で使えるようになりました。

ただこれだけだとエラー処理がしづらいところがあるので、「非同期処理の結果と例外が発生すればその例外情報を保持するようなクラスを作ってそのクラスにラッピングして返す」というような工夫が必要かもです。
(なにかこれ以外でいい方法があれば教えてほしいです。)

と、こんなかんじでAndroid Arch(以下略)とkotlin Coroutineを組み合わせてAndroidのライフサイクルと仲良くする方法を書きましたが。 この中で「ここをこうすればもっと良い」や「ここがおかしい」、「別にこんなことしなくても、もっといい方法がほかにあるよ」っていうのがあれば教えていただければ幸いです。 ここの日本語がおかしいとかの添削も大歓迎です。(驚くべきことに僕は日本人です)

以下、Jobとライフサイクルのバインドを行なう関数のあれのGistです。
”_BindLaunch.kt”