📘

RepositoryやDataSourceでFlowをshareIn/stateInする

2024/06/29に公開

概要

Android アプリの開発をしていると Repository や DataSource のようなデータレイヤーで SharedFlow や StateFlow を作りたくなることがあります。
Firebase Firestore、AndroidX Room や Realm のようなデータベースの変更監視を行う場合にこうした状況があるかと思います。

通常、こうした変更監視ではただの Flow を生成して呼び出し元に返すことは容易にできますが、一方で複数の画面にまたがって共有されるような Shared/StateFlow を返すことはあまり簡単ではありません。

この記事ではデータレイヤーで Shared/StateFlow を扱うことが難しい理由と、その解決策を提示したいと思います。

※ Dagger HiltによるDependency Injectionを利用している前提です

データレイヤーで Shared/StateFlow を扱いづらい理由

データレイヤーで Shared/StateFlow を扱いづらいことにはいくつかの理由があります。

  1. 複数 Fragment にまたがって有効な CoroutieScope が必要になるが、大抵は ViewModel 経由で利用されるため Fragment 用の ViewModel の CoroutineScope では扱えない
  2. Activity の ViewModel を複数 Fragment で共有しても良いが、そうしてしまうと「Fragment 固有だけれど共有したいデータ」を扱う際に Activity ViewModel を経由しなければならなくなり凝集度が下がるし Activity ViewModel が肥大化してしまう
  3. 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