【Kotlin】SharedFlowとStateFlowを読み解く

## 前置き

[ACCESS Advent Calendar 2020](https://qiita.com/advent-calendar/2020/access) の9日目です。
[初学者向けKotlin Coroutines Flow](https://qiita.com/knagauchi/items/12aa1a4400256cece72c)の続きで、SharedFlowとStateFlowに関する記事です。

## Flowの復習

Flowはこういうやつでしたね。

```
View    |  ViewModel    | UseCase〜深層
-----------------------------------------------
        |               |
イベント  →  Coroutine起動 → 時間のかかる処理（非同期）
　　　      　|　　　　　　　　           |       ↓↓↓
描画  ←←←  出力データ加工 ←←← 出力データ送信（複数回）
        |               |
```

```kotlin:CounterUseCase.kt
suspend fun countStream(): Flow<Int> = flow {
    repeat(100) { count ->
        delay(100)
        emit(count) // 0 1 2 3 4 ... 99
    }
}
```

※View/ViewModelのコードは省略。[こちら](https://qiita.com/tonionagauzzi/items/12aa1a4400256cece72c#flow%E3%81%AE%E5%9F%BA%E6%9C%AC%E7%9A%84%E3%81%AA%E4%BD%BF%E3%81%84%E6%96%B9)に載ってます。

Flowでの値の更新は、上記UseCaseの`flow { ... }`ラムダ式中でしかできません。
つまりViewModel側では**値を更新できず**、また`.value`みたいに**値の参照もできません**。
Subscribeしてる数だけ`flow { ... }`ラムダ式が呼ばれてしまうのも特徴です。

それだと状態保持とか処理リソースの節約には向いてないってことで、ホットストリームなFlowとして登場したのが、今回紹介する**SharedFlow**と**StateFlow**です。

## SharedFlowとは

複数箇所でのSubscribeでデータや状態を共有できるFlowで、処理リソースの節約に向いてます。
ただのFlowと違う点は以下。

```Kotlin
sharedFlow.onEach {
    println("1")
}.launchIn(scope)

sharedFlow.onEach {
    println("2")
}.launchIn(scope)
```

* こんな感じで複数Subscribeしてても`flow { ... }`ラムダ式側は1回しか呼ばれない
* 処理開始/Subscribe終了のタイミングを選択できる（[後述](https://qiita.com/knagauchi/private/43dcb01f9f120e14cc23#%E5%87%A6%E7%90%86%E9%96%8B%E5%A7%8B%E3%82%BF%E3%82%A4%E3%83%9F%E3%83%B3%E3%82%B0%E3%81%AE%E6%8C%87%E5%AE%9A)）が、これを適切に指定しないとSubscribeされ続ける
  * [このサンプル](https://developer.android.com/kotlin/flow/stateflow-and-sharedflow?hl=ja#livedata)
  （`LatestNewsActivity`のとこ）みたいに`LiveData`に変換し、`observe`引数に`LifecycleOwner`を設定すれば、表示中だけのSubscribeも可能
* 色々高機能
  * replay: Subscribeした瞬間、過去のn回の値を受信する
  * buffer: 複数Subscribeかつ処理に時間がかかるとき、1回目に行われた処理をバッファリングして、2回目以降を早くしてくれる（一言では説明しにくい…[こちら](https://at-sushi.work/blog/25)がわかりやすい）

https://gist.github.com/manuelvicnt/6f14fd66dd000d2e76fcbc4695cd836a

↑このように書くことで、1つだけのFlowインスタンスを全ての場所で利用でき、監視は必要な間だけ動作します。また、[この](https://gist.github.com/manuelvicnt/c08046c8ddf29157a981df254c2862c1#file-locationrepository-kt)ように書き換えると永続的に監視しつつ、`replay`で最後に発行された10個を常に受信します。

  
## StateFlowとは

状態保持に特化したSharedFlowです。
LiveDataに似ていますが、LiveDataはAndroid、SharedFlowはKotlinのフレームワークです。とはいえ、LiveDataの後継機能として使うこともできます。

* 初期値が必須
* 現在の状態を`.value`で受け取れる
* MutableStateFlowを使えば、`.value`への代入も可能
  * その際Coroutines Scopeは不要
* `launchIn`で直近の値を1つ受信する
* 同じ値の代入は受信しない
* waitとかを挟まず連続して値が変更されたとき、最後の値しか受信しない
  * つまり「状態」が「保持」されないと「状態変化」とみなされない

LiveDataとの違いは、[こちらの記事](https://star-zero.medium.com/livedata%E3%81%A8stateflow%E3%81%AE%E9%81%95%E3%81%84-5c141c6eb0f9)がわかりやすいです。

`sharedFlow`では、Viewを開いたタイミングでflowが処理中（サーバー通信中とか）だったら、直近の値をどう表示するのかで迷います。しかし、`stateFlow`では`.value`に直近の値がキャッシュされているので、迷わずに済みます。

## 初期化方法

`MutableSharedFlow`（[link](https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/-mutable-shared-flow/)）、`MutableStateFlow`（[link](https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/-mutable-state-flow/index.html)）を使い、[このように](https://developer.android.com/kotlin/flow/stateflow-and-sharedflow?hl=ja#sharedflow)初期化します。

もしくは、`shareIn`（[link](https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/share-in.html)）、`stateIn`（[link](https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/state-in.html)）を使い、[このように](https://developer.android.com/kotlin/flow/stateflow-and-sharedflow?hl=ja#sharein)変換します。`shareIn`は`sharedFlow`インスタンスを、`stateIn`は`stateFlow`インスタンスを返します。

## 注意点

関数の戻り値で`shareIn`や`stateIn`をしてはなりません。それをすると、関数の呼び出しごとに新しい`SharedFlow`または`StateFlow`が作成され、リソースの無駄遣いになります。

また、[この](https://gist.github.com/manuelvicnt/de2a31d207a606bdb2240d33cdc00c72#file-userrepository-kt)`userId`みたいな1つの入力に対して1つの処理結果をシーケンシャルに返すFlowは、ホットストリーム化して複数のSubscriberがいると誤動作する可能性があります。`shareIn`や`stateIn`で安易に共有してはならないパターンです。

## 処理開始タイミングの指定

`shareIn`や`stateIn`はFlowをホットストリーム化するので、`flow { ... }`ラムダ式の処理開始タイミングを指定できます。[SharingStarted](https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/-sharing-started/index.html)オプションで、

1. `shareIn`や`stateIn`の際に開始して永続的に有効な`Eagerly`
2. Subscribeが行われた際に開始して永続的に有効な`Lazily`
3. Subscribeが行われた際に開始して、Subscribeされている間だけ有効な`WhileSubscribed`

を選択することができます（[詳細はこちら](https://developer.android.com/kotlin/flow/stateflow-and-sharedflow?hl=ja#sharein)）

## 結局どれがいいのか

…は、場合によって異なります。大事なのは、要件に応じて`Flow`/`SharedFlow`/`StateFlow`を適切に使い分けることです。

どうしても迷うときは、

1. Subscribe場所の結果に狂いが生じないこと
2. リソースの無駄遣いにならないこと

を念頭に置いて判断しましょう。

## 関連記事

* [結局Coroutines FlowはLiveDataより強いのか](https://zenn.dev/tonionagauzzi/articles/6319a1db2034ac)
* [モバイルアプリ開発と単方向データフローの話](https://zenn.dev/tonionagauzzi/articles/be9d39bf2f9718)
* [Kotlin Coroutines FlowをSwiftでobserveしてみた](https://zenn.dev/articles/1041232711083d)

## あとがき

本記事の内容が、関連記事とあわせて技術書典12新刊の[ACCESSテックブック2](https://techbookfest.org/product/5187915669831680?productVariantID=5039132097118208)に収録されました！

## 参考文献

* [StateFlow と SharedFlow](https://developer.android.com/kotlin/flow/stateflow-and-sharedflow?hl=ja)
* [SharedFlow - kotlinx-coroutines-core](https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/-shared-flow/)
* [StateFlow - kotlinx-coroutines-core](https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/-state-flow/)
* [kotlin coroutinesのFlow, SharedFlow, StateFlowを整理する](https://at-sushi.work/blog/24)
* [SharedFlowの深堀り、replay, bufferって何](https://at-sushi.work/blog/25)
* [LiveDataとStateFlowの違い](https://star-zero.medium.com/livedata%E3%81%A8stateflow%E3%81%AE%E9%81%95%E3%81%84-5c141c6eb0f9)
* [Substituting Android’s LiveData: StateFlow or SharedFlow?](https://proandroiddev.com/should-we-choose-kotlins-stateflow-or-sharedflow-to-substitute-for-android-s-livedata-2d69f2bd6fa5)
* [Things to know about Flow’s shareIn and stateIn operators](https://medium.com/androiddevelopers/things-to-know-about-flows-sharein-and-statein-operators-20e6ccb2bc74)