LoginSignup
6
4

More than 1 year has passed since last update.

CoroutineDispatcherをHiltでいい感じにDIする

Posted at

概要

CoroutineのCoroutineDispatcherをHiltでDIできるようにした方法です。

解決したいこと

まず前提としてCoroutinesをAndroidで使用する際に下記のように実装することが好ましいとされています。
また今回は1と3の問題を解決するためにCoroutineDispatcherをDIする必要性が出てきました。

  1. suspend関数はmain-safeな実装にすること
  2. suspend関数はcancellableな実装にすること
  3. 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の実態を振り分けられるようにした
6
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
4