Edited at

[Google I/O 2019] LiveData を Kotlin-Coroutineと一緒に使う


背景

Google I/O 2019で発表されたLiveDataをKotlin-Coroutineと一緒に使う方法が今の悩みにジャストフィットしてしまったので、早速使う。


状況:Roomからのfetchをコルーチンで行いたい


  • ある情報を取ってくるRepositoryがある。これは以下のような機能を持っている。


    • Roomに値が保存されていない場合はAPIからデータを取得する。

    • Roomに値が保存されていて、かつ最後に取得した日時から1時間過ぎていればAPIからデータを取得する

    • それ以外の場合はRoomから値を取得する

    • APIから取得した場合はRoomにまず保存し、その後RoomからLiveData(Paging)を返す



この時RoomのLiveData連携を利用しようとすると、以下のように書いておきたい。


TestViewModel.kt

class TestViewModel(private val repository: Repository) : ViewModel() {

val liveData = repository.fetch()
}

しかしrepositoryはAPI通信を伴うためワーカースレッドで動作させる必要がある。そのためどうにかしてコルーチンを使いたい。


build.gradleを設定する

利用にはlivedata-ktx:2.2.0-alpha01(2019/5/8時点)が必要。


build.gradle

    implementation "androidx.lifecycle:lifecycle-extensions:2.2.0-alpha01"

implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0-alpha01"
implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.2.0-alpha01"


実装

以下のように実装する。


TestViewModel.kt


class TestViewModel(private val repository: Repository) : ViewModel() {

var List: LiveData<List<Data>> = liveData(context = Dispatchers.IO, timeoutInMs = 3_000L) {
emitSource(repository.fetch()) // 今回はRoomとLiveDataが連携しているため最初から戻りがLiveData.その場合はemitSource()を利用する
}

class Factory(private val repository: Repository) : ViewModelProvider.NewInstanceFactory() {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
return TestViewModel(repository) as T
}
}
}


コメントにも記載しているが、RoomがLiveData連携している場合はfetch時にLiveDataが返却される。それをemitする場合はemitSource()を利用する。

なお、emitSourceはMainThreadで動いてくれる模様。

以下はliveData()が返却するLiveDataScopeの実装部分だが、coroutineContextのDispatchersがMainになってる。


LiveDataScopeImpl.kt


internal class LiveDataScopeImpl<T>(
internal var target: CoroutineLiveData<T>,
context: CoroutineContext,
override val initialValue: T? = target.value
) : LiveDataScope<T> {
// use `liveData` provided context + main dispatcher to communicate with the target
// LiveData. This gives us main thread safety as well as cancellation cooperation
private val coroutineContext = context + Dispatchers.Main

override suspend fun emitSource(source: LiveData<T>): DisposableHandle =
withContext(coroutineContext) {
return@withContext target.emitSource(source)
}

override suspend fun emit(value: T) = withContext(coroutineContext) {
target.clearSource()
target.value = value
}
}



動作の注意事項(liveData内コメント引用)


  • liveDataブロックは返却したLiveDataがactiveになった時に実行される。

  • liveDataブロックの実行中にLiveDataが非アクティブになると、タイムアウトになる前にLiveDataが再度アクティブにならなかった場合、timeoutInMsミリ秒後にキャンセルされる。

  • タイムアウトによるキャンセルが発生した場合、LiveDataが再びActiveになったタイミングでliveDataブロックは最初から実行される。

  • LiveDataが非アクティブになる以外の理由でliveDataブロックが正常に終了/キャンセルされた場合、LiveDataがアクティブになっても再実行されない。


感想

丁度欲しかったものがこのタイミングで来てとても嬉しい。