新しいViewModelを作るときに、uiState
を必ずといって良いほど定義します。
その際に毎回必ずstateIn()
を使ってMutableStateFlow
に変換してからコンポーザブルで利用するのですが、いつもこれは一体何をしているんだろうとモヤモヤしながら書いていました。
そこで今回は、将来の自分がまたFlow
やStateFlow
で迷子にならないための忘備録として、難しいことは一切省いたStateFlow
の解説記事を残しておこうと思います。
KotlinにおけるFlowとは一体なんなの?
そもそも、概念的な意味におけるKotlinでのFlowとは、suspend関数を使って非同期に値を生成し、その生成された値を必要としているレイヤー側にて使用するまでの一連の流れのことを表します。
そして、この一連の流れであるFlowは、以下の3つの要素で成り立っています。
- Producer(プロデューサー)
- Intermediary(インターミディアリー)
- Consumer(コンシューマー)
FlowのProducerとは
プロデューサーは、データを生成するためのものです。
そしてこれは、Flowビルダーを使って作成します。
プロデューサーからコンシューマー側に値を送信するためには、emit()
を使用します。
fun flowExample() {
val flow = flow<String> {
emit("Hello")
delay(3000)
emit("World")
}
...
}
FlowのIntermediaryとは
プロデューサー側から、emit()
の呼び出しによって送信されたデータを、変更することができる中間レイヤーに該当する存在のことをインターミディアリーと言います。
しかし、このインターミディアリーは必要な場合にのみ作成される感じのようなので、今回は具体的な使用例や説明を省略させていただきます。
FlowのConsumerとは
emit()
の呼び出しによって、送信された値を受け取る存在がコンシューマーとなります。
ここで重要なのが、Flowそれ自体はコールドストリームに該当するということです。つまり、コンシューマー側で明示的に値を取得しようとしない限りは、プロデューサー側での値の生成は行われません。
つまり値が欲しい場合には、常にコンシューマーを作成または用意をして、明示的に値を取得しようとする必要があります。
fun flowExample() {
val flow = flow<String> {
...
}
val job = viewModelScope.launch {
flow.collect() {
delay(3000)
println("送信された内容 > $it")
}
}
}
上記のプロデューサーの場合は次のようなログが表示されるはずです。
送信された内容 > Hello
送信された内容 > World
おまけ: バックプレッシャーとは
バックプレッシャーとは、コンシューマー側がプロデューサー側から送信されたデータを処理しきれていない状態のことで、新たにプロデューサー側からデータを送信されることによりデータの損失が発生してしまう現象のことを言います。
collect()
に渡したラムダにて、明示的にdelay()
を使うことでこの問題をを回避する方法がよく使われています。
Flowに対してのStateFlowの存在とは
上記で説明した通り、純粋なFlowでは、コンシューマ側で値を取得しようとした時でしか値が生成されません。つまり、明示的にcollect()
を呼び出さない限りは、最新の値を取得することができないのです。これは、Flowがコールドストリームだからです。
そのコールドストリームであるFlowをホットストリームにしたものがStateFlowになります。ホットストリームであることにより、StateFlowでは明示的なコンシューマーを必要としません。つまり、コンシューマーがいなくとも値を生成することが可能になるのです。
SharedFlowとStateFlowの違いとは
StateFlowと比較されるものに、SharedFlowがあります。
実は、StateFlowとはSharedFlowの概念を拡張したものになります。
SharedFlowについての具体的な説明は割愛させていただきますが、StateFlowはSharedFlowと違い、常に最新の値が1つ格納され、value
プロパティを通して値にアクセスしたり設定したりすることが可能となります。
ちなみに、SharedFlowのreplay
に1
を設定した場合はStateFlowと同じように動作させることができます。
SharedFlowのreplayとは
ShareFlowのreplay
に指定する値によって、値を収集開始したタイミングで直近何件のデータを取得するかを設定できます。デフォルトでは0になっているため、収集開始したタイミングで取得できるデータは0になります。つまり、それ以降にemit()されたデータが取得対象になります。
どうしてAndroid開発では、StateFlowがよく利用されるのか
AndroidでLiveDataに変わるものとして、StateFlowが利用され始めた背景には、ViewModelの状態を管理・表現しやすいということがあります。ViewModelの状態を監視して、その値に基づいてUIを更新したいときには、StateFlowが他のFlowに比べて使いやすいという背景があるのです。
StateFlowとSharedFlow、どっちを使えば良いの?
StateFlowでは常に最新の値のみを取得できるようになっているため、emit()
を呼び出して送信されたすべてのイベントを損失なく取得できることは保証されていません。このため、すべてのイベントを取得したい場合にはSharedFlowの利用が推奨されています。
stateInを使ってStateFlowを作成する例
StateFlowを作成するときには、必ず初期値が必要になります。
コンシューマー側は、常に最新の値を、StateFlowを通して取得することが可能になります。
ViewModelでStateFlowを使用する場合は、Flowに対してstateIn()
を呼び出すことでStateFlowを作成します。
class ExampleViewModel() : ViewModel() {
private val viewModelState = MutableStateFlow(
ExampleViewModelState(
isLoading = false,
errorMessage = "",
)
)
val uiState = viewModelState
.map(JournalEntriesPracticeViewModelState::toUiState) // ここで返されるのはFlow
.stateIn(
viewModelScope,
SharingStarted.Eagerly,
viewModelState.value.toUiState()
) // ここで返されるのがMutableStateFlow
Flowから値を収集するために、コルチーンを起動する必要があります。そのため、stateIn関数の第一引数には、コルーチンスコープを渡す必要があります。
第二引数には、どのタイミングの値からリッスンを開始すべきかを指定します。
第三引数には初期値を指定します。
SharingStartedとは
上記で説明した通り、stateIn()
の第二引数にはSharingStarted
を指定します。このSharingStarted
とは、共有コルーチンを開始、および終了させるためのストラテジーであるとドキュメントでは説明されています。組み込みストラテジーのセットとしてEagerly
、Lazily
、WhileSubscribed
の3つのストラテジーが提供されています。
SharingStarted.Eagerlyとは
SharingStarted.Eagerly
を指定した場合は、stateIn()
を呼び出したタイミングで値の生成を開始します。注意点としてはコンシューマー側でcollectAsState()
を呼び出したタイミングによって、最初にemit()
された値などを取得できない可能性があります。
SharingStarted.Lazlyとは
SharingStarted.Lazyly
を指定した場合は、最初にコンシューマー側によってcollectAsState()
が呼び出されたタイミングで値を生成します。これにより、最初に取得しようとしたコンシューマーは今までに発行されたすべての値を取得することが保証されます。
SharingStarted.WhileSubscribedとは
SharingStarted.WhileSubscribed
は、最初にコンシューマー側によってcollectAsState()
が呼び出されたタイミングで値を生成します。しかし他のオプションと違い、cancel()
などが呼び出されることにより、値を必要としているコンシューマー側の数が0になった時点で値の生成を辞めますので注意が必要です。
コンポーザブルがコンシューマー側だったときの例
コンポーザブルでStateFlow
の値を収集する場合は、collectAsState()
を呼び出します。
@Composable
fun ExampleRoute(
viewModel: ExampleViewModel = hiltViewModel(),
) {
val uiState by viewModel.uiState.collectAsState()
このメソッドが呼び出された時点で取得できる値は、stateInの第二引数に指定したオプションによって異なります。
まとめ
今まではStateFlowやFlowの違いがわからないまま、なんとなく難しいものと思いつつ使っていました。今回改めて調べて自分にとってわかりやすいようにまとめることで、StateFlowやFlow、そしてSharedFlowのそれぞれが使われるべき箇所や背景を表面だけでも理解することができたのでとても良かったと思います。
Flowはそこまで怖くないよ!
参考にした記事