前置き
ACCESS Advent Calendar 2020 の9日目です。
初学者向けKotlin Coroutines Flowの続きで、SharedFlowとStateFlowに関する記事です。
Flowの復習
Flowはこういうやつでしたね。
View | ViewModel | UseCase〜深層
-----------------------------------------------
| |
イベント → Coroutine起動 → 時間のかかる処理(非同期)
| | ↓↓↓
描画 ←←← 出力データ加工 ←←← 出力データ送信(複数回)
| |
suspend fun countStream(): Flow<Int> = flow {
repeat(100) { count ->
delay(100)
emit(count) // 0 1 2 3 4 ... 99
}
}
※View/ViewModelのコードは省略。こちらに載ってます。
Flowでの値の更新は、上記UseCaseのflow { ... }
ラムダ式中でしかできません。
つまりViewModel側では値を更新できず、また.value
みたいに値の参照もできません。
Subscribeしてる数だけflow { ... }
ラムダ式が呼ばれてしまうのも特徴です。
それだと状態保持とか処理リソースの節約には向いてないってことで、ホットストリームなFlowとして登場したのが、今回紹介するSharedFlowとStateFlowです。
SharedFlowとは
複数箇所でのSubscribeでデータや状態を共有できるFlowで、処理リソースの節約に向いてます。
ただのFlowと違う点は以下。
sharedFlow.onEach {
println("1")
}.launchIn(scope)
sharedFlow.onEach {
println("2")
}.launchIn(scope)
- こんな感じで複数Subscribeしてても
flow { ... }
ラムダ式側は1回しか呼ばれない - 処理開始/Subscribe終了のタイミングを選択できる(後述)が、これを適切に指定しないとSubscribeされ続ける
-
このサンプル
(LatestNewsActivity
のとこ)みたいにLiveData
に変換し、observe
引数にLifecycleOwner
を設定すれば、表示中だけのSubscribeも可能
-
このサンプル
- 色々高機能
- replay: Subscribeした瞬間、過去のn回の値を受信する
- buffer: 複数Subscribeかつ処理に時間がかかるとき、1回目に行われた処理をバッファリングして、2回目以降を早くしてくれる(一言では説明しにくい…こちらがわかりやすい)
↑このように書くことで、1つだけのFlowインスタンスを全ての場所で利用でき、監視は必要な間だけ動作します。また、このように書き換えると永続的に監視しつつ、replay
で最後に発行された10個を常に受信します。
StateFlowとは
状態保持に特化したSharedFlowです。
LiveDataに似ていますが、LiveDataはAndroid、SharedFlowはKotlinのフレームワークです。とはいえ、LiveDataの後継機能として使うこともできます。
- 初期値が必須
- 現在の状態を
.value
で受け取れる - MutableStateFlowを使えば、
.value
への代入も可能- その際Coroutines Scopeは不要
-
launchIn
で直近の値を1つ受信する - 同じ値の代入は受信しない
- waitとかを挟まず連続して値が変更されたとき、最後の値しか受信しない
- つまり「状態」が「保持」されないと「状態変化」とみなされない
LiveDataとの違いは、こちらの記事がわかりやすいです。
sharedFlow
では、Viewを開いたタイミングでflowが処理中(サーバー通信中とか)だったら、直近の値をどう表示するのかで迷います。しかし、stateFlow
では.value
に直近の値がキャッシュされているので、迷わずに済みます。
初期化方法
MutableSharedFlow
(link)、MutableStateFlow
(link)を使い、このように初期化します。
もしくは、shareIn
(link)、stateIn
(link)を使い、このように変換します。shareIn
はsharedFlow
インスタンスを、stateIn
はstateFlow
インスタンスを返します。
注意点
関数の戻り値でshareIn
やstateIn
をしてはなりません。それをすると、関数の呼び出しごとに新しいSharedFlow
またはStateFlow
が作成され、リソースの無駄遣いになります。
また、このuserId
みたいな1つの入力に対して1つの処理結果をシーケンシャルに返すFlowは、ホットストリーム化して複数のSubscriberがいると誤動作する可能性があります。shareIn
やstateIn
で安易に共有してはならないパターンです。
処理開始タイミングの指定
shareIn
やstateIn
はFlowをホットストリーム化するので、flow { ... }
ラムダ式の処理開始タイミングを指定できます。SharingStartedオプションで、
-
shareIn
やstateIn
の際に開始して永続的に有効なEagerly
- Subscribeが行われた際に開始して永続的に有効な
Lazily
- Subscribeが行われた際に開始して、Subscribeされている間だけ有効な
WhileSubscribed
を選択することができます(詳細はこちら)
結局どれがいいのか
…は、場合によって異なります。大事なのは、要件に応じてFlow
/SharedFlow
/StateFlow
を適切に使い分けることです。
どうしても迷うときは、
- Subscribe場所の結果に狂いが生じないこと
- リソースの無駄遣いにならないこと
を念頭に置いて判断しましょう。
関連記事
あとがき
本記事の内容が、関連記事とあわせて技術書典12新刊のACCESSテックブック2に収録されました!
参考文献
- StateFlow と SharedFlow
- SharedFlow - kotlinx-coroutines-core
- StateFlow - kotlinx-coroutines-core
- kotlin coroutinesのFlow, SharedFlow, StateFlowを整理する
- SharedFlowの深堀り、replay, bufferって何
- LiveDataとStateFlowの違い
- Substituting Android’s LiveData: StateFlow or SharedFlow?
- Things to know about Flow’s shareIn and stateIn operators