🙆

Kotlin Coroutines FlowのSharingStartedの使い分け

2022/05/19に公開

Coroutines Flowを使用した時Hot化する際に思わぬ不具合を発生させてしまったので共有します。
基本的にはAndroid DevelopersのFlowのドキュメントに書いてあるような内容です。
https://developer.android.com/kotlin/flow/stateflow-and-sharedflow?hl=ja#sharein

Cold,Hotストリームとは

Cold, HotストリームはRxなどでよく出てくる概念で、
Cold, Hotではストリーム(リソース)の取得と解放のタイミングが異なります。

Coldストリーム

ColdストリームはStreamをSubscriptionしたタイミングで
リソースの取得やイベントが流されるようになるストリームです。
ストリームが使用されなくなると自動的に解放されるようになっていたりすることが多いです。
Coroutines FlowだとchannelFlowなどがこれにあたります。
用途としては使用しなくなっタイミングでunregisterする必要があるAPIなどを使用する時に使用します。

  • ネットワークの通信
  • WebSocket
  • Androidのregister系
    • registerDefaultNetworkCallbackなど
@RequiresApi(Build.VERSION_CODES.N)
@ExperimentalCoroutinesApi
fun ConnectivityManager.activeNetworkFlow(): Flow<Boolean> {

    return channelFlow {

        val callback = object : ConnectivityManager.NetworkCallback() {
            override fun onAvailable(network: Network) {
                super.onAvailable(network)
                trySend(true)
            }

            override fun onLost(network: Network) {
                super.onLost(network)
                trySend(false)
            }
        }
        registerDefaultNetworkCallback(callback)
        
	// Flowが使われなくなると呼び出される
        awaitClose {
            unregisterNetworkCallback(callback)
        }
    }
}

Hotストリーム

HotストリームはSubscriptionの状態に関わらず、
リソースの取得やイベントが流されるストリームです。
用途としてはSubscriptionの状態に関わらずイベントを流したい時や、
状態を入れておきたい時に使用します。

Coroutines FlowのColdストリーム

Coroutines Flowはflowに対してstateInやshareInなどの拡張関数を呼び出すことによって
Hotストリームに変換することができます。
この時にSharingStartedというFlowの購読状態を制御するインスタンスを注入するのですが、
こちらの指定を間違うと思わぬ不具合やリソースの解放漏れが発生することがあるので注意が必要です。

StateFlow, SharedFlowの違い

channelFlowはSubscribeは1つしかすることができませんが、
SharedFlowは複数のSubscribeをサポートしています。
StateFlowは複数のSubscribeとLiveDataのように直接値を代入、取り出す機能をサポートしています。
Rxで例えると以下のように例えることができると思います(微妙に挙動が違いますが)

  • SharedFlow = PublishSubject
  • StateFlow = ReplaySubject
    また最近ではStateFlowがLiveDataの大替として使用される場面を目にすることが多くなったような気がします。

stateIn, shareInの使い方

基本的にはstateIn, shareInで変換をすることができます。
stateInの場合は変数のように値を取り出せる必要性があるため初期値を入れておく必要があります。
※初期値を待ってstateInを返すといったこともできます。

// StateFlowに変換
anyFlow.stateIn(coroutineScope, SharingStarted.Eagerly, initialValue)

// SharedFlowに変換
anyFlow.shareIn(coroutineScope, SharingStarted.Eagerly)

発生した問題

標準で提供されているSharingStartedにはEagerlyとLazilyとWhileSubscribedがあります。
これらをユースケースに応じて適切に使い分ける必要があったのですが、
なんとなく使用していたため思わぬ不具合が発生してしまいました。

それぞれの違い

Eazily, Lazilyの違いとしては開始の条件が違うことが挙げられます。
stateInを使用する場合Eazily, Lazily初期値が流れるタイミングが微妙に違うため
EditTextなどのViewと連携する際思わぬ値が流れてクラッシュしたりすることがあるので注意が必要です。
またEazily, LazilyはSubscriptionされなくなっても解放されず、基本的にCoroutineScopeに合わせて解放されるので注意が必要です。
WhileSubscribedはsubscriptionCountで取得と解放が行われるので、
Subscriptionの状況に応じて解放をしたいときはこちらを使用します。
CoroutineScopeと合わせて解放したいときはEazily、Lazilyで良いのですが、
購読状態に合わせて取得、解放を行いたいときはWhileSubscribedを使用する必要があります。

  • Eazily
    • SharedFlow, StateFlowに変換した時点でFlowが開始されます
    • FlowはCoroutineScopeがcancelされたタイミングで解放されます
  • Lazily
    • 変換したFlowのSubscribeを開始した時点でFlowが開始されます
    • FlowはCoroutineScopeがcancelされたタイミングで解放されます
  • WhileSubscribed
    • subscriptionが1つ以上ある時にFlowが開始されます
    • subscriptionが1つ未満になったタイミングで解放されます

まとめ

  • FlowをHot化したいときはstateIn, shareInを使用する。
  • SharingStartedは購読状態の制御パターンを指定している
  • SharingStartedはユースケースに合わせ選択する必要がある

Discussion