Spica*

プログラミングの話。

Kotlin Flow完全に理解したので、LiveDataで困っていたことを書く

むおおおお! 久しぶりに個人ブログを更新したけど、 個人的に脳内を整理するために書き散らしたエントリなので役に立たないかも。

Kotlin Flowを勉強する場合、例えば以下のサイトを流し読む方が役には立つハズ…

勉強するときに見たサイト

このエントリを書いた背景とか

  • ViewModelやModelといったビジネスロジックレイヤーやデータレイヤーからデータをもらう時の手段としてAndroid LiveDataを使っている。頭が古くなっていて、LiveDataから抜け出せずにいる
  • LiveDataでは困ってるところがわりと出てきていて、そろそろKotlin Flow移行したい気持ちが湧いて出ている

LiveDataで困ってるところ

LiveDataはobserveしないとintermediaryが実行できない

Kotlin Flow

上記はKotlin Flowの最初に出てくる図だけど、 上記の図の各要素は、LiveDataでは以下のように例えられる。

class MyView : LifecycleOwner {
    val dataSource = MyDataSource()

    fun main() {
        // ③
        dataSource.displayUpdatedAt.observe(this) { value ->
            // TODO display value
        }
    }

    override fun getLifecycle() = TODO("Not yet implemented")
}

class MyDataSource {
    private val _updatedAt = MutableLiveData<OffsetDateTime>() // ①
    val displayUpdatedAt = _updatedAt.map {
        it.format(DateTimeFormatter.ISO_DATE_TIME) // ②
    }

}
  • ①がProducer
  • ②がIntermediary
  • ③がConsumer

このとき、LiveDataは、③のコードを書いていないと、②が実行されないという特徴がある。(Kotlin Flowは使い方次第)

これはLiveDataの仕様である。LiveDataはあくまでUIに値を表示するために作られたもの。 ②はUIのために値を加工するプロセスであり、(LiveData的には)③がなければ②は実行する必要がないのだ。 これはLiveDataのいいところであり、③のコードが実行されるまでは、①が実行されても②を実行しないので、②がコストのかかる処理だった場合、無駄な処理をしなくて済む。

しかしこの挙動が困ることがある。例えば①の値が更新されたことを契機に、MyDataSource内で何らかの処理(データベースのアクセスやネットワークアクセスなど)をしたいことが時々ある。 それを実現するために、③をわざわざ書かざるを得ないことがある。(当然その目的の場合、observe()の中身は空実装になる) LiveData「だけ」だと、実現できないことがあるのが問題。

Kotlin Flowの方が細かいことができること、 Kotlin Flow(というよりFlow Builderかな?)はLiveDataに変換することもできることを考えると、  基本的にはKotlin FlowのAPIを使って、LiveDataとして扱いたいときは .asLiveData() ( androidx.lifecycle  |  Android Developers )を使ってLiveDataとして振る舞うようにするのがよいのではないかと考えている。

(後半は推測) Eventの扱いが面倒

LiveDataは、前述の①を変更すると③の関数が実行される。 しかし、①を変更したタイミングだけでなく、 ①で値を持っている状態で③を実行する(=observeを行う)と、そのタイミングでも③のobserverの関数が実行される。

これはネットワーク通信を行った結果を一度だけ受信する時に、問題になる。 例えばネットワーク通信後のデータをLiveDataで保持し、画面回転すると、実際にはネットワーク通信をしてないのにネットワーク通信後のデータが③のobserverの関数に流れる。

これをLiveDataを使って避ける方法については、既に答えが出ていて、Eventラッパーを使うのが正解なのだが、いかんせん記述が長くなるのが気になっている… 例えばネットワーク通信部分では、以下のような定義を行う。

class MyDataSource(
    private val api: RestApiService
) {
    private val _myList = MutableLiveData<Event<Resource<List<MyEntity>>>>()
    // ....(以下省略)

    suspend fun fetchData() {
        _myList.value = Event(Resource.Loading(_myList.value?.peekContent()?.data))
        try {
            val result = api.myRestApi().response()// TODO ネットワークからREST APIのデータ取得
            _myList.value = Event(Resource.Success(result))
        } catch (e: IOException) {
            _myList.value = Event(Resource.Error(e, _myList.value?.peekContent()?.data))
        }
    }
}

(ここからは推測) Kotlin Flowは、

他の中間演算子で指定しない限り、Flow は「コールド」かつ「遅延」となります。 https://developer.android.com/kotlin/flow?hl=ja

ということなので、observeした時に命令を実行する挙動は避けることが出来そう。 つまりEventラッパーはなくすことができて、シンプルに記述することができるようになりそう。

Resourceラッパーは https://developer.android.com/kotlin/flow/stateflow-and-sharedflow?hl=ja を見る限り

// Represents different states for the LatestNews screen
sealed class LatestNewsUiState {
    data class Success(val news: List<ArticleHeadline>): LatestNewsUiState()
    data class Error(val exception: Throwable): LatestNewsUiState()
}

のような記述があるのでどうだろうなーと思いつつ、 Kotlin FlowではexceptionをFlowで流せるので、Resourceもいらなくなるかもなーと感じている。 シンプル・イズ・ベスト。余計なグルーコードなどはなくしていきたい所存。

(これは推測) LiveDataはAndroidのための仕組みなので、Kotlin Multiplatform(KMP)などで障害になる

LiveDataはLifecycleOwnerを指定することで、AndroidのLifecycleを監視して、 onStart()〜onResume()の間でのみobserve()の中身を実行するようになっている。 (これはDialogの表示などで例外が発生しないようにする時なんかで特に便利に使っている)

LiveDataはAndroidのための仕組みであり、AndroidAPIに依存してしまう。 Kotlin MultiplatformといったAndroid非依存向けの環境も(将来含め)ターゲットに入れる場合、 ロジックが結局Androidに依存した処理となってしまい、障害になってしまうのではないかと思っている。

対してKotlin FlowはAndroidではなくKotlinという言語自体に含まれる仕組みであるため、 例えばiOS(Kotlin Native)やWeb(Kotlin/JS)などでも扱えるのではないかと思っている。 (ただ、Kotlin MultiplatformもKotlin NativeもKotlin/JSも、実際に扱ったわけではなく、コンセプトだけ見て想像でものを言ってるので、正しくない可能性はある)

まあ、それを言い出すと例えばMVVMでいうところのViewModelやModelレイヤーでは ContextやJava 8 Date/Time APIなどにも依存しないコードを書く必要があるはずなので 視野に入れるかどうかの判断のほうが先に重要にはなりそうではあるが…。

ということで

Kotlin Flowには徐々に移行していきたいお気持ち。

一気に変更することってなかなかできないのよね。 チームに納得してもらう必要があるし、テストしなおしだし。 事前知識が大量にいる上に、お金と時間の調整もしないといけないから、 積極的にマイグレーションしているブログなんかを見るとすげえなって思う。 がんばってこ。