LoginSignup
1
2

Android+ViewModel+coroutine+roomの使い方2点

Last updated at Posted at 2023-02-15

Android で coroutineを使って書いていて、直面した問題

  1. 画面に検索条件があって、その検索条件で絞って room を検索し、その結果を RecyclerView に表示したい。RecyclerView に行を追加する EditText、buttonもあり、追加する場合は今、表示されている検索条件が room のテーブルのキーになっているので、そのキーで room に INSERT。RecyclerView も再表示したいので、ここは observe パターンでなければいけない。observe パターンで画面から検索条件が変わる場合って、どうすればいいんだろう?
  2. 大量データを INSERT する処理がある。処理時間が長いので、その間は room のデータを変更して欲しくない。処理開始前にボタンをdisable にし、progressbar を表示。処理が完了するとボタンを enable、progressbar を非表示にしたい。coroutine の非同期処理の終了時に画面の View を更新するようにするには、どうしても ViewModel の中で context の参照が必要になる。ViewModel の中での context の参照はメモリリークになるので、ViewModel の外(Activity、Fragment)で View を更新するようにするにはどうすればいいのか?

LiveData で返して、observe している最中に検索条件が変わる場合

google の公式サンプルRoomWordsSample
では、room のテーブル全件を SELECTしています。

WordRepository.kt
val allWords: Flow<List<Word>> = wordDao.getAlphabetizedWords()
WordViewModel.kt
val allWords: LiveData<List<Word>> = repository.allWords.asLiveData()
MainActivity.kt
wordViewModel.allWords.observe(this) { words ->
    // Update the cached copy of the words in the adapter.
    words.let { adapter.submitList(it) }
}

これが画面から検索条件を入力する場合、どのように書けばいいでしょうか?
単純に、wordDao.getAlphabetizedWords() に引数を増やして、画面から渡るようにしてもうまくいきません。
何故なら、このやり方だと、画面で検索条件が変わるたびに、MainActivity で observe を呼ぶことになります。observe を複数回呼ぶと前に呼ばれた observe が破棄されて、新しい obsrve が開始されるのではなく、呼ばれた回数分だけ複数並列で observe が開始されます。
結果、検索条件の違う複数の observe がほぼ同時に処理されるのでタイミングによってどの observe の結果が画面に返るか、コロコロ変わると言う、おかしな状態になってしまいます。

Transformations.switchMap() を使って解決する

ヒントが google 公式のサンプルにありました。
Using Kotlin Coroutines in your Android app
この中の、PlantListViewModel.ktの使い方を見ると参考になります。
https://github.com/googlecodelabs/kotlin-coroutines/blob/master/advanced-coroutines-codelab/finished_code/src/main/java/com/example/android/advancedcoroutines/PlantListViewModel.kt

この中で LiveData#switchMap を使っている箇所があります。

switchMap についてはここ、公式ホームページ:LiveData の概要 → LiveData を変換する

これを使います。Transformations は LiveData を変換するのに使います。LiveData の拡張関数にもなっています。関数がふたつあり。

  • map:
public inline fun <X, Y> LiveData<X>.map(
    crossinline transform: (X) -> Y
): LiveData<Y>
  • switchMap:
public inline fun <X, Y> LiveData<X>.switchMap(
    crossinline transform: (X) -> LiveData<Y>
): LiveData<Y>

これらは、いずれも LiveData を変換する関数で LiveData<X> → LiveData<Y> への変換を行っています。違うのは、引数の関数 transform の戻りの型が map が Y 型なのに対し、switchMap が LiveData<Y> になっています。

今回の要件を実現するために、

  • LiveData<X> を画面からの検索条件
  • LiveData<Y> を検索結果

にします。つまり画面からの検索条件が変われば、LiveData<X> の中身の条件を読み取って、その条件で room の DAO の検索を呼び出し、その結果を LiveData<Y> に設定すれば、LiveData の中身が入れ替わるということです。

そのためには、transform の戻りの型が Y 型の map よりも、LiveData<Y> 型の switchMap の方が都合がいいです。

viewmodel.kt
// 検索条件のLiveData
private val searchCond = MutableLiveData<String>()

// MainActivityから検索条件を設定してやる関数
fun setSelectCond(searchCd: String) {
    selectCond.value = searchCd
}

// 検索する関数
fun selectData(): LiveData<SomeEntity> =
    searchCond.switchMap { searchCd ->
        if (searchCd.isBlank()) {
            MutableLiveData()
        } else {
            repository.selectByCode(searchCd).asLiveData()
        }
    }

MainActibityy.kt
// 画面から取ってきた検索条件を設定する
viewModel.setSelectCond(hogehoge)

・・・

// 検索結果のObserve
viewModel.selectData().observe(viewLifecycleOwner) { someEntity ->
    // 結果をViewに表示してやる
    displayData(someEntity)
}

この例は検索条件が1個の場合ですが、複数ある場合は、

private val searchCond = MutableLiveData<String>()

ここの LiveData のジェネリック型を String じゃなくて、独自の data class にすればよいです。

coroutine の非同期処理の完了後に view を更新したい場合

もうひとつの問題。

  1. 非同期で長い処理があります。その処理中に画面の他のボタンを押してほしくない。
  2. 画面のボタンを disable にして progressbar を表示してから、非同期処理を呼び出す。
  3. 非同期処理が終わると画面のボタンを enable、progressbar を非表示

こいった場合、たいていの場合は ViewModel 側でこのような書き方をします

fun insertAll(list: List<SomeEntity>) {
    viewModelScope.launch(Dispatchers.IO) {
        repository.insert(list)
        // ここでINSERTが終わった時に、viewを更新したいんだが・・・
    }
}

上記のように INSERT が終わった時点で、画面のボタンを enable、progressbar を非表示したいのですが、ViewModel の中で context のインスタンスを参照するとメモリリークの原因になります。View を更新するためにはどうしても context が必要になります。

かと、言って MainActivity 側に、

viewModel.insertAll(list)
// ここでviewを更新しても非同期処理が終わる前に実行されてしまう。

上記のように書いても非同期処理が終わる前に実行されてしまうので、意味がありません。

こういった場合、StateFlow を使います。
StateFlow は kotlin corouteinsの1.3.6で追加されています
SharedFlow は kotlin corouteinsの1.4.0-M1で追加されています。

StateFlow、SharedFlow、Flowについて

公式ホームページ:StateFlow と SharedFlow

StateFlow と SharedFlow の具体的な動作の違いはこのページの解説がわかりやすいです。
KotlinのSharedFlowを図で理解する
KotlinのSharedFlowとStateFlowの違いを理解する

StateFlow はその名の通り「State」(状態)保持用の監視可能な Flow です。それに対し、
SharedFlow はデータ保持用の監視可能な Flow です。

上の例で、大量INSERTが「完了」したことの「状態」を監視すればいいので、この場合は StateFlow の方が適切です。

viewmodel.kt
// INSERT完了のStateFlow
private val _insertDone = MutableStateFlow(false)
val insertDone: SharedFlow<Boolean>
    get() = _insertDone

・・・

fun insert(list: List<SomeEntity>) {
    viewModelScope.launch(Dispatchers.IO) {
        repository.insert(list)
        // ここでINSERTが終わった時、通知する
        _insertDone.emit(true)
    }
}
MainActivity
lifecycleScope.launch {
    repeatOnLifecycle(Lifecycle.State.STARTED) {
        medViewModel.mstSyncDone.collect { status ->
           if (status) {
              // true 完了
           } 
        }
    }
}

StateFlow の場合 LiveData の observerではなくて、collect を使います。ラムダの引数に viewModel の StateFlow のジェネリック型である Boolean が渡ってきますので、それをみて大量 INSERT が終わったかどうかを判定します。

最後に、StateFlow、SharedFlow、Flow、LiveData についてもうちょっと掘り下げてみたい

これまで説明した通り、FlowファミリーのStateFlow と SharedFlow の使いわはなんとなくわかったと思います。

SharedFlow と LiveDataですが、どちらも監視可能なデータフォルダで、似通っています。ですが、両者でここが異なります。

  • StateFlow では初期状態をコンストラクタに渡す必要がありますが、LiveData ではその必要はありません。
  • LiveDataは、View のライフサイクルが STOPPED になるとコンシューマが自動的に登録解除されます。が、StateFlow は、収集は自動的には停止されません。同じ動作を実現するには、Lifecycle.repeatOnLifecycle ブロックで囲う必要があります。

repeatOnLifecycle は lifecycle-runtime-ktx:2.4.0-alpha01 で追加されています。
それ以前は、launchWhenStarted を使っていましたが、repeatOnLifecycle に置き換わるのでこちらが推奨されます。

StateFlow は SharedFlow を継承しています。なので動作の面での特性は SharedFlow を継承しているので、StateFlow と SharedFlow はペアになります。

SharedFlowは作成時のパラメータ

  • replay
  • extraBufferCapacity
  • onBufferOverflow

で細かい動作を指定することができます。この辺は LiveData よりも優れているのでは?

StateFlow も SharedFlow も Flowからの作成が可能になっています。

  • Flow#shareIn
  • Flow#stateIn

一方、LiveData は SharedFlow に置き換え可能です。

ということを考えると、StateFlow と SharedFlow が主流で、LiveData は SharedFlow に置き換えられていくのでは?ないかとおもいますが、どうなんでしょうか?

1
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
2