Infinito Nirone 7

白羽の矢を刺すスタイル

Dagger Hilt と WorkManager を組み合わせて使う

Dagger Hilt は Android 用に様々な場面で使いやすいようになっていて、WorkManager を使った場合にも Dagger Hilt で DI が実現できるようになっています。

依存関係

Dagger Hilt と WorkManager を依存関係に追加します。2021/12 時点で Dagger Hilt の KSP 対応は完了していないので、kapt を使います。

buildscript {
    dependencies {
        // Hilt Gradle Plugin への依存を追加
        classpath("com.google.dagger:hilt-android-gradle-plugin:2.40.3")
    }
}
plugins {
    // kapt を適用
    kotlin("kapt")
    // Hilt Gradle Plugin を適用
    id("dagger.hilt.android.plugin")
}

dependencies {
    // Dagger-Hilt の依存
    implementation("com.google.dagger:hilt-android:2.40.3")
    kapt("com.google.dagger:hilt-android-compiler:2.40.3")
    // AndroidX Hilt の依存
    implementation("androidx.hilt:hilt-common:1.0.0")
    kapt("androidx.hilt:hilt-compiler:1.0.0")
    implementation("androidx.hilt:hilt-work:1.0.0")
    // WorkManager の依存
    implementation("androidx.work:work-runtime-ktx:2.7.1")
}

kapt {
    correctErrorTypes = true
}

Worker の定義

Worker の定義は次の通りで、@HiltWorker, @AssistedInject, @Assisted の 3 つのアノテーションを使います。 - @HiltWorker: クラス定義に使うアノテーション - @AssistedInject: Worker のコンストラクタに使うアノテーション - @Assisted: WorkManager 内部で Worker に渡される Context と WorkerParameters に対して使うアノテーション

@HiltWorker
class SampleWorker @AssistedInject constructor(
    @Assisted applicationContext: Context,
    @Assisted params: WorkerParameters,
) : Worker(applicationContext, params) {
    override fun doWork(): Result {
      // ...
    }
}

この Worker に対し、さらに別のオブジェクトを差し込みたい場合は単純にコンストラクタに差し込みたいオブジェクトを追加していくだけです。

@HiltWorker
class SampleWorker @AssistedInject constructor(
    @Assisted applicationContext: Context,
    @Assisted params: WorkerParameters,
    // なんらかの data source レイヤのクラスを注入する(別途この data source の実体を提供するための module の宣言などは必要)
    private val sampleDataSource: SampleDataSource,
) : Worker(applicationContext, params) {
    override fun doWork(): Result {
      // ...
    }
}

WorkerFactory の追加

WorkManager では Work クラスの生成はほぼ自分で記述することがありません。Worker の定義がシンプルでコンストラクタの引数に Context と WorkerParameters しかない場合は、WorkRequest を経由して自動で Worker がインスタンス化されます。一方で引数に Context と WorkerParameters 以外のものがある場合は WorkManager が自動で Worker を生成できないため、Worker を生成するための手順を実装し WorkManager に教えてあげる必要があります。

Dagger Hilt を使う場合、先ほどの SampleWorker を生成するには Dagger が知っている object graph から SampleDataSource の実体を取り出して Worker にわたす手順が必要ですが、その手順は hilt-work が用意してくれていて、アプリの実装としては Application クラスを拡張し必要なインタフェースの実装を追加するだけになります。

@HiltAndroidApp
// androidx.work の Configuration.Provider を実装する
class ExampleApplication : Application(), Configuration.Provider {

    // Dagger Hilt 用の WorkerFactory
    @Inject lateinit var workerFactory: HiltWorkerFactory

    // Dagger Hilt 用の WorkerFactory を設定して Configuration を返す
    override fun getWorkManagerConfiguration() =
        Configuration.Builder()
            .setWorkerFactory(workerFactory)
            .build()
}

この設定のため AndroidManifest.xml では WorkManager の default initializer を無効化しておきます。default initializer の無効化手順は、アプリで App Startup を使っているかどうかによって変わります。WorkManager は内部で App Startup の仕組みを使っているため、アプリが WorkManager 以外の用途で App Startup を使っている場合は、その App Startup から WorkManager のみを無効化する必要があります。

<!-- アプリで他に AppStartup を使わない場合 -->
<provider
    android:name="androidx.startup.InitializationProvider"
    android:authorities="${applicationId}.androidx-startup"
    tools:node="remove">
</provider>
<!-- アプリで他に AppStartup を使っている場合 -->
<provider
    android:name="androidx.startup.InitializationProvider"
    android:authorities="${applicationId}.androidx-startup"
    android:exported="false"
    tools:node="merge">
    <meta-data
        android:name="androidx.work.WorkManagerInitializer"
        android:value="androidx.startup"
        tools:node="remove" />
</provider>

WorkRequest を使って動かす

あとは Worker を動かす手順を実装するだけです。注意点として、WorkManager のインスタンスは必ず WorkManager.getInstance(Context) を使って取得します(引数のない getInstance メソッドでは WorkerFactory の設定などが無視されてしまう)。

val delay = 1000L
val params = Data.Builder()
    .putInt("number", 1)
    .build()

val workManager = WorkManager.getInstance(Context)
val work: OneTimeWorkRequest = OneTimeWorkRequest.Builder(SampleWorker::class.java)
    .setInitialDelay(delay, TimeUnit.MILLISECONDS)
    .setInputData(params)
    .build()
workManager.beginUniqueWork("sample_work", REPLACE, work).enqueue()