概要
CoroutineのCoroutineDispatcherをHiltでDIできるようにした方法です。
解決したいこと
まず前提としてCoroutinesをAndroidで使用する際に下記のように実装することが好ましいとされています。
また今回は1と3の問題を解決するためにCoroutineDispatcherをDIする必要性が出てきました。
- suspend関数はmain-safeな実装にすること
- suspend関数はcancellableな実装にすること
- withContextのCoroutineDispatcherはハードコードしないこと
main-safeな実装について
suspend関数はMainスレッドから呼び出されても問題ない(クラッシュしたりフリーズしない)実装にすることが好ましいとされています。
withContextのCoroutineDispatcherはハードコードしないこと
ハードコードしないことが好ましいとされている理由としては、
CoroutineDispatcherは環境によっては動作しないことがあり、
Android環境では問題なく動作しても、単体テストの環境ではうまく動作しないということがあります。
そのためテストの時はテスト用のDispatcherを指定することがあるのですが、
ハードコードしてしまうとDispatcherをテスト用のものに置き換えることができず、
Unitテストでテストすることができないという問題が発生してしまいます。
Hiltではデフォルト値を指定することができない
まずCoroutineDispatcherを差し込むために、
コンストラクタにCoroutineDispatcherの項目を追加し、デフォルト値でDispatchers.*を指定するようにしました。
しかしHiltはデフォルト値をサポートしていないため、コンパイルすることができません
class NewsRepository @Inject constructor(
private val defaultDispatcher: CoroutineDispatcher = Dispatchers.Default
) {
suspend fun loadNews() = withContext(defaultDispatcher) { /* ... */ }
}
CoroutineDispatcherをprovideする
次に試した方法としてHiltのModuleで任意のCoroutineDispatcherをProvidesしてしまう方法を取りました。
しかしこれにも問題があります。
このように指定してしまうと、今後CoroutineDispatcherをHiltで解決する時に、毎回Dispatchers.IOが帰ってきてしまいます。
一人開発で十分気をつけて開発する時は良いですが、他のメンバーがDispatchers.Mainを取得できると勘違いし、想定していない実装をしてしまった場合クラッシュなどが発生してしまう可能性があります。
また当然ですが下記例の場合だとDispatchers.IO以外のCoroutineDispatcherをDIできなくなってしまいます 。
@InstallIn(SingletonComponent::class)
@Module
object CoroutineDispatcherModule {
@Provides
fun provideCoroutineDispatcher(): CoroutineDispatcher {
return Dispatchers.IO
}
}
解決した方法
Hiltの「同じ型に複数のバインディングを提供する」を用いて、
アノテーションで必要なCoroutineDispatcherを指定してDIするようにしました。
https://developer.android.com/training/dependency-injection/hilt-android?hl=ja#multiple-bindings
まずDIしたいCoroutineDispatcherのアノテーションを作成します。
今後CoroutineDispatcherのDIを行う時にこのアノテーションを指定して必要なインスタンスを取得するようになります。
import javax.inject.Qualifier
@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class IODispatcher
@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class MainDispatcher
@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class DefaultDispatcher
@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class UnconfinedDispatcher
次にCoroutineDispatcherを解決するためのModuleを作成します。
provideメソッドに実態となるインスタンスに対応したアノテーションを付与してあげます。
@InstallIn(SingletonComponent::class)
@Module
object CoroutineDispatcherModule {
@DefaultDispatcher
@Provides
fun provideDefaultDispatcher(): CoroutineDispatcher {
return Dispatchers.Default
}
@IODispatcher
@Provides
fun provideIODispatcher(): CoroutineDispatcher {
return Dispatchers.IO
}
@MainDispatcher
@Provides
fun provideMainDispatcher(): CoroutineDispatcher {
Dispatchers.Unconfined
return Dispatchers.Main
}
@UnconfinedDispatcher
@Provides
fun provideUnconfinedDispatcher(): CoroutineDispatcher {
return Dispatchers.Unconfined
}
}
使用例
class NewsRepository @Inject constructor(
@IODispatcher private val ioDispatcher: CoroutineDispatcher
) {
suspend fun loadNews() = withContext(ioDispatcher) { /* ... */ }
}
まとめ
- Coroutinesには好ましいとされるお作法がある
- main-safeであること
- withContextにCoroutineDispatcherをハードコードしてはいけない
- Hiltではそのままでは同一の型を振り分けることができない
- Hiltのアノテーションを使った方法でCoroutineDispatcherの実態を振り分けられるようにした