RepositoryやDataSourceでFlowをshareIn/stateInする
概要
Android アプリの開発をしていると Repository や DataSource のようなデータレイヤーで SharedFlow や StateFlow を作りたくなることがあります。
Firebase Firestore、AndroidX Room や Realm のようなデータベースの変更監視を行う場合にこうした状況があるかと思います。
通常、こうした変更監視ではただの Flow を生成して呼び出し元に返すことは容易にできますが、一方で複数の画面にまたがって共有されるような Shared/StateFlow を返すことはあまり簡単ではありません。
この記事ではデータレイヤーで Shared/StateFlow を扱うことが難しい理由と、その解決策を提示したいと思います。
※ Dagger HiltによるDependency Injectionを利用している前提です
データレイヤーで Shared/StateFlow を扱いづらい理由
データレイヤーで Shared/StateFlow を扱いづらいことにはいくつかの理由があります。
- 複数 Fragment にまたがって有効な CoroutieScope が必要になるが、大抵は ViewModel 経由で利用されるため Fragment 用の ViewModel の CoroutineScope では扱えない
- Activity の ViewModel を複数 Fragment で共有しても良いが、そうしてしまうと「Fragment 固有だけれど共有したいデータ」を扱う際に Activity ViewModel を経由しなければならなくなり凝集度が下がるし Activity ViewModel が肥大化してしまう
-
MainScope()
が返す CoroutineScope だと画面破棄時に処理を中断することができない
こうした理由があるためデータレイヤーで Shared/StateFlow を提供する構造にしづらいのですが、一方でデータレイヤーの変更監視でこそ Flow を使いたいし、いろいろな画面で共有できる構造であってほしいわけです。
上記の理由をまとめると「データレイヤーのスコープに対応した適切な CoroutineScope が存在していない」ことに大きな問題があり、実は「データレイヤーのスコープに対応した適切な CoroutineScope があれば問題を解決できそうだ」と言えそうです。
では、「データレイヤーのスコープに対応した適切な CoroutineScope」とはどういったものでしょうか?
データレイヤーのための CoroutineScope
データレイヤーが必要とする CoroutineScope は「Activity または Activity ViewModel のライフサイクルと(ほぼ)同じライフサイクルである」ことを満たせば、データレイヤーで扱いやすい Shared/StateFlow を作れます。
Dagger Hilt を利用して Dependency Injection を行っている場合、ActivityRetainedLifecycle
を利用して CoroutineScope を生成することで上記の条件を満たした CoroutineScope を作成できます。
@Module
@InstallIn(ActivityRetainedComponent::class)
class DataLayerModule {
@Named("ActivityRetained")
@ActivityRetainedScoped
@Provides
fun providesActivityRetainedCoroutineScope(lifecycle: ActivityRetainedLifecycle): CoroutineScope {
val scope = object : CoroutineScope {
override val coroutineContext: CoroutineContext = SupervisorJob() + Dispatchers.Main.immediate
}
lifecycle.addOnClearedListener {
scope.coroutineContext.cancel()
}
return scope
}
}
こんな感じの Module を用意して使用する側は
class SomeRepository @Inject constructor(
@Named("ActivityRetained") coroutineScope: CoroutineScope,
dataSource: SomeDataSource,
) {
val someDataFlow = dataSource
.updates()
.shareIn(coroutineScope, SharingStarted.WhileSubscribed)
}
こんな感じで Shared/StateFlow を提供することができます。
Discussion