LoginSignup
21
8

More than 1 year has passed since last update.

【Kotlin】SharedFlowとStateFlowを読み解く

Last updated at Posted at 2020-12-08

前置き

ACCESS Advent Calendar 2020 の9日目です。
初学者向けKotlin Coroutines Flowの続きで、SharedFlowとStateFlowに関する記事です。

Flowの復習

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

View    |  ViewModel    | UseCase〜深層
-----------------------------------------------
        |               |
イベント  →  Coroutine起動 → 時間のかかる処理(非同期)
          |                   |       ↓↓↓
描画  ←←←  出力データ加工 ←←← 出力データ送信(複数回)
        |               |
CounterUseCase.kt
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として登場したのが、今回紹介するSharedFlowStateFlowです。

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に直近の値がキャッシュされているので、迷わずに済みます。

初期化方法

MutableSharedFlowlink)、MutableStateFlowlink)を使い、このように初期化します。

もしくは、shareInlink)、stateInlink)を使い、このように変換します。shareInsharedFlowインスタンスを、stateInstateFlowインスタンスを返します。

注意点

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

また、このuserIdみたいな1つの入力に対して1つの処理結果をシーケンシャルに返すFlowは、ホットストリーム化して複数のSubscriberがいると誤動作する可能性があります。shareInstateInで安易に共有してはならないパターンです。

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

shareInstateInはFlowをホットストリーム化するので、flow { ... }ラムダ式の処理開始タイミングを指定できます。SharingStartedオプションで、

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

を選択することができます(詳細はこちら

結局どれがいいのか

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

どうしても迷うときは、

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

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

関連記事

あとがき

本記事の内容が、関連記事とあわせて技術書典12新刊のACCESSテックブック2に収録されました!

参考文献

21
8
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
21
8