Blog

LiveDataのHot, Coldって意識できている?

最近のandroid開発において、複雑なlifecycleに対応するのにLiveData は重要な役割を果たすことが多いです。

その他のstream系ライブラリと比べても非常にシンプルで、初心者でも比較的親しみやすいと思います。

一方で、意外なところで躓くことも多いのが実情かと思います(経験談)。

今回はHot, Coldの話を主軸に、LiveDataの躓きやすいポイントについて触れたいと思います。

Hot Observable / Cold Observableとは?

rx(RreactiveX)kotlin coroutinesにはHot Observable, Cold Observableという考え方があります。

詳細な説明は省きますが、Cold Observableはobserveすると動作し、Hod Observableはobserveしなくても動作しています。

rxでは、Published Subject, Behavior Subject等のSubjectはHot Observableであり、map, filter等のoperatorはほとんどがCold Observableです。

また、replayやshareといったHot Observableに変換するためのoperatorも用意されています。

kotlin coroutinesではもっと単純で、channelはHot、flowはColdです。

これらの使い分けは難解ですし、よく初心者殺しと言われますが、streamを扱う上で非常に重要な考えになっています。

LiveDataはHot? Cold?

肝心のLiveDataの話に戻りましょう。

LiveDataはいわゆるObservableですが、これはHot Observableなのか、Cold Observableなのかどちらでしょう?

この問題は難しいので、一旦次のコードを見て、実行結果を予想してほしいです。

val liveData1 = MutableLiveData<Int>()
val liveData2 = liveData1.map {
    println(it)
    it.toString()
}
liveData1.value = 1
liveData1.value = 2

正解は、 何も表示されません

では、次はどうでしょう?

val liveData1 = MutableLiveData<Int>()
val liveData2 = liveData1.map {
    println(it)
    it.toString()
}
liveData2.observeForever {
    // NOP
}
liveData1.value = 1
liveData1.value = 2

今度は、以下のように出力されます。

1
2

なるほど、observeしないと動かないので、mapはCold Streamなのでしょうか?

では、こちらはどうでしょう?liveData2を2回observeしています。

val liveData1 = MutableLiveData<Int>()
val liveData2 = liveData1.map {
    println(it)
    it.toString()
}
liveData2.observeForever {
    // NOP
}
liveData2.observeForever {
    // NOP
}
liveData1.value = 1
liveData1.value = 2

これもまた、一つ前と同様な結果が得られます。

1
2

一見自然のように見えますが、本来のCold Observableなら、observeする度に動くので、以下のように2回ずつ表示されるはずです。

1
1
2
2

これは、内部でactiveになっているobserverの数を数えていて、複数回observeしても一度のみmap内が実行されるようになっています。

rxでいう refCount がこれに当たります。

observeしていない間はデータが流れず、Cold Observableのような振る舞いをしますが、observeしている間はHod Observableのような振る舞いをするわけです。

ちなみにswitchMap, distinctUntilChanged, MediatorLiveDataのaddSrouce内も同様の挙動をします。

また、当然ですがMutableLiveDataはobserveしていなくても動いており、完全にHot Observableです。

間違いやすい例

上記のような特徴をちゃんと把握していないと、間違った記述をしてしまい、予期せぬ動きをします。

valueが最新でない

次の実行結果は何になるでしょう?

val liveData1 = MutableLiveData<Int>()
val liveData2 = liveData1.map {
    it.toString()
}
liveData1.value = 1
println(liveData2.value)

正解は、以下のとおりです。

null

liveData2はobserveしない限り動かないので、valueも当然nullのままになります。

これの厄介なところは、どこかでliveData2をobserveしていると、正しく動いているように見えることです。

ただ、何かのコード改変でobserveしなくなったとき、突如として動かなくなります。

liveDataのvalueは、current valueではなく、last valueであることを意識し、MutableLiveData以外では使わないほうが良いでしょう。

val liveData1 = MutableLiveData<Int>()
println(liveData1.value.toString())

operatorに副作用をもたせている

以下のような記述をしたことはありませんか?

val source1 = MutableLiveData<Int>()
val source2 = MutableLiveData<Int>()
val target = MutableLiveData<Int>()
target.observeForever {
  println(it)
}

val out1 = liveData1.map {
    target.value = it * 2
    it.toString()
}
val out2 = liveData2.map {
    target.value = it * 3
    it.toString()
}
source1.value = 1
source2.value = 1

もう分かると思いますが、こちらは当然、 何も表示されません

しかし、これもout1, out2をobserveしていると正しく動いているように見えます。

複数個のLiveDataから一つのデータを作りたいときに、やってしまいがちだと思います。

こちらは、MediatorLiveDataを使うのが良いと思います。

val source1 = MutableLiveData<Int>()
val source2 = MutableLiveData<Int>()
val target = MediatorLiveData<Int>()
target.addSource(source1) {
    target.value = it * 2
}
target.addSource(source2) {
    target.value = it * 3
}
target.observeForever {
  println(it)
}
source1.value = 1
source2.value = 1

以下のような結果を得ることができます。

2
3

lifecycleにも注意

LiveDataのobserveは通常、lifecycleに従ってobserveされます。

そのため、activityがbackgroundになったonStopからforegroundになるonStartまでの間、LiveDataはobserveされず、operatorは動作しません。

検証のため、以下のようなDummyLifecycleOwnerを用意します。

class DummyLifecycleOwner : LifecycleOwner {
    private val registry = LifecycleRegistry(this)

    override fun getLifecycle(): Lifecycle {
        return registry
    }

    var currentState: Lifecycle.State
        get() = registry.currentState
        set(value) {
            registry.currentState = value
        }
}

そして、例えばこのようなコードを書きます。

val owner = DummyLifecycleOwner()
val liveData1 = MutableLiveData<Int>()
val liveData2 = liveData1.map {
    println(it)
    it.toString()
}
liveData2.observe(owner) {
    // NOP
}

// 初期
owner.currentState = Lifecycle.State.INITIALIZED
liveData1.value = 1
liveData1.value = 2
// onCreate
owner.currentState = Lifecycle.State.CREATED
liveData1.value = 3
liveData1.value = 4
// onStart
owner.currentState = Lifecycle.State.STARTED
liveData1.value = 5
liveData1.value = 6
// onResume
owner.currentState = Lifecycle.State.RESUMED
liveData1.value = 7
liveData1.value = 8
// onPause
owner.currentState = Lifecycle.State.STARTED
liveData1.value = 9
liveData1.value = 10
// onStop
owner.currentState = Lifecycle.State.CREATED
liveData1.value = 11
liveData1.value = 12
// onDestroy
owner.currentState = Lifecycle.State.DESTROYED
liveData1.value = 13
liveData1.value = 14

結果はこうなります

4
5
6
7
8
9
10

LiveDataはobserve時に最後に流れた値をreplayするため、onCreate時の4も処理されます。

全てのデータが処理されるわけではないことに注意してください。

そのため、LiveDataをevent streamのように使うのはやめましょう。

observeForeverを使えば、全てのデータが流れてきますが、購読解除が難しいと思います。

LiveDataはそのために設計されていないので、kotlin coroutinesやrx javaの導入を検討しましょう。

まとめ

今回は、LiveDataでハマりやすい点をHot, Coldの面から確認しました。

LiveDataの特性をしっかり理解して、どのタイミングでどのブロックが実行されているのか把握することが大事だと思います。

また、利用ケースによってはkotlin coroutinesやrx javaと使い分けるようにしましょう。

一方、kotlin coroutinesは最近lifecycle scopeに対応したので、実はLiveDataなしでもMVVMが実現できるのではないかと考えています。

今後、そういったsampleも書いてみようと思います。

人気の記事

kotlin coroutinesのFlow, SharedFlow, StateFlowを整理する

Jetpack ComposeとKotlin Coroutinesを連携させる

Layout Composableを使って複雑なレイアウトを組む【Jetpack Compose】

テスト用Dispatcherの使い分け【Kotlin Coroutines】

Flow.combineの内部実装がすごい話

Jetpack ComposeのRippleエフェクトを深堀り、カスタマイズも