SharedFlow, StateFlowの違いについて
StateFlowはSharedFlowの特殊版。
- 初期値を有する
- 新しいSubscriberには最新の値をemit
- 最新値以外のCacheはしない
- resetReplayCacheをサポートしない
等が主な違いです。公式ドキュメントは以下
より詳細には以下のPostが分かりやすいです。
日本語ならQiitaのこれ。
SharedFlow(StateFlow)かObserverパターンか
そもそも考えたいのが以下のパターンでした。
この構成自体がよろしくない可能性がありますが、ViewModel1はTopbarに対応するViewModelであり、ViewModel2よりも長いLifeCycleを保持しています。一方で、ViewModel2はViewModel1の値に応じて表示内容を変更したいため、ViewModel1に依存する必要があります。
上図のようにDirectで依存させるのはHiltでは不可能
まず初めに考えたのはとりあえずそのまま依存させれば動くのでは?(イベントがRepository層とViewModel層の両方に通達されるため、不良な設計と言える)と思いとりあえず実装しようと思いましたが、異なるViewModelを一方に依存させる形で依存性注入を行うのはHiltでは不可能でした。
例えば以下のように実装したとします。
@HiltViewModel class ViewModel1 @Inject constructor( private val repo: IRepository ): ViewModel() @HiltViewModel class ViewModel2 @Inject constructor( private val repo: IRepository private val viewModel1: ViewModel1 ) : ViewModel()
この場合以下のようなErrorでInjectできません。
Injection of an @HiltViewModel class is prohibited since it does not create a ViewModel instance correctly. Access the ViewModel via the Android APIs (e.g. ViewModelProvider) instead.
また、hiltViewMode<ViewModel1>()を使用して依存性を注入しようとしても以下のようなErrorで不可能です。
@Composable invocations can only happen from the context of a @Composable function
言われてみれば、設計レベルでは確かにViewModel1がViewModel2よりもLifeCycleが長いことは保証されていますが、そんなことはコードからは単純には分からないのでErrorが出るのは全うだと納得しました。(Error文からはViewModelProvider等を使用すれば適切にInjectできそうな雰囲気を感じましたが、そもそも綺麗な設計ではないので諦めました)
Repositoryから順次Updateする
以下のように設計することとしました。Androidで推奨される形式と同じです。
RepositoryからViewModel2へのUpdateはRepository層のほうがLifeCycleが勿論長いので、そのあたりを適切に処理した手法を採用する必要があります。
SharedFlow(StateFlow)かObserverパターン
ここで題名に戻る訳ですが、別にこれってSharedFlowやStateFlowを使用せずとも一般的なObserverパターンでViewModel2がRepositoryをObserveする形式でも実現できます。新しい手法は学習コストもありますが、Androidで定石となっているSharedFlow(StateFlow)を把握しておきたかったので調査しました。
結果的にはやはりSharedFlow(StateFlow)を使用した方が簡潔で拡張性もあるのでいいと思います。
Observerパターン
古典的なObserverパターンを考えます。
@HiltViewModel class ViewModel2 @Inject constructor( private val repo: IRepository private val viewModel1: ViewModel1 ) : ViewModel(), IObserber { init{ repo.addObserver(this) } override fun onUpdate(){ doSomething() } override fun onCleared(){ repo.removeObserver(this) } }
ViewModel2だけに限ると上記のようになると思います。Construct時にObserveして、clearされるときに解放します。勿論、ここで解放しないとメモリリークになってしまうので、忘れないように実装する必要があります。
また、独自のObserver interfaceを用意する必要もあるのでコード量が単純に多いです。
SharedFlow(StateFlow) の場合
以下のように実装します。(本来はlatestSomethingは_latestSomething:SharedFlowも定義しBackingPropertyとして外部からのemit操作を禁止すべきです)
@HiltViewModel class ViewModel2 @Inject constructor( private val repo: IRepository ) : ViewModel(){ init{ repo.latestSomething .onEach { doSomething(it) }.launchIn(viewModelScope) } @Singleton class Repository @Inject constructor( private val externalScope: CoroutineScope, private val defaultDispatcher: CoroutineDispatcher ): IRepository{ val latestSomething = MutableSharedFlow<String>(replay = 0) override suspend fun updateSomething(newSomething: String): Unit = withContext(defaultDispatcher){ latestSomething.emit(newSomething) } }
このように実装することでlatestSomethingの更新をViewModel2で受け取れるようになります。また、ViewModel2以外でもlatestSomethingの更新通知が必要となった場合にはまったく同様の手段で通知を行うことが可能です。
(ちなみにreplay=0としているのは、collect時には値の取得は必要なく新しい通知のみ受け取りたいためです。)
また、SharedFlow(StateFlow)のいい点として、呼び出し元CoroutineScopeが終了した場合、自動でunsubscribeされることにあります。したがって、Observerパターンの際に記載したようなremove忘れによるメモリーリークの心配をする必要がありません。例の場合、ViewModel2がonCleared()された時点でunsubscribeされます。(正確なタイミングは未調査です)
ちなみにSharedStateやStateFlowにunsubscribeというメソッドは用意されていないようです。Subscriberの数自体は#subscriptionCountで確認できます。
結論
一般に使用されているライブラリを使用したほうがコードは簡潔になるので使用すべき、となりました。
今回は特に注目したのはメモリーリークのみですが、SharedFlowとすることで複数Subscriberへの通知、複数Producerからの合流、subscribe時の動作等、様々に規定することができるのでやはり高名なライブラリは使用した方がいいです。
勿論学習コストもかかるので、非常に限定的な使用に限られる場合は自前で実装した方が早い場合もありますが・・。ベストプラクティスを学習するのはどうしても時間がかかってしまいますね。