fresh digitable

セミコロンたちが躍動する おいらのコードを 皆さんに 見せたいね

特定のタスクに依存したりさせたりしたいがflavorがいっぱいあって手で全部書くのがつらい時

project.afterEvaluate のブロックの中なら評価後のすべてのタスクを列挙できる。例えば特定のFlavorのあるタスクの前にしたいことがあるなら

project.afterEvaluate {
    tasks.findAll { ... }.forEach { t ->
        t.dependsOn(...)
    }
}

WorkManagerのWorkerにDagger2でDIする

今回の前提:

implementation "androidx.work:work-runtime-ktx:2.3.1"
api "com.google.dagger:dagger:2.26"
kapt "com.google.dagger:dagger-compiler:2.26"

WorkerWorkManagerによって生成される。デフォルトではContextWorkerParametersが渡されて初期化される。初期化の際、それ以外に必要なオブジェクトを受け取りたいときにはWorkerFactoryを実装してWorkManagerに渡してやる必要がある。独自のWorkerFactoryを実装して使えるようにするには、次のことが必要。

  • AndroidManifest.xmlWorkManagerInitializerを止める。例えば: implemented scheduled worker for record schedule checker · akihito104/nasnen@c3b1d22 · GitHub
  • ApplicationクラスにConfiguration.Providerを実装する。Configuration.BuilderWorkerFactoryをセットしてbuild()し、Configuration.Provider.getWorkManagerConfiguration()から返せるようにする。
  • WorkManager.getInstance(Context)を使ってWorkManagerを取得する。Contextを受け取らない方はDeprecatedになっている。

戦略はViewModelの時と大体同じで、Daggerが作ってくれるProviderのMapをWorkerFactoryに注入して、WorkerFactory.createWorker()の中でProviderを取り出して生成したクラスを返してやればよい。ただし、ViewModelの場合はDIが知っているオブジェクトしか取り扱わないのでDIが初期化までやってくれたのだが、Workerの場合はWorkerFactory.createWorker()に渡されるContextWorkerParametersを渡して初期化する必要があるので勝手が違う。

Providerから出てきたものにContextWorkerParametersを渡して初期化をしたいので、Providerから出てくるものは具体的なWorkerを作るFactoryである必要がある。例えば:

class HogeWorker(
    private val repository: HogeRepository,
    context: Context,
    param: WorkerParameters
) : CoroutineWorker(context, param) { // 継承するListenableWorkerは何でもいいです
// ...
  class Factory @Inject constructor(
      private val repository: HogeRepository
  ) : HogeWorkerFactory.Factory {
      override fun create(
          appContext: Context,
          workerParameters: WorkerParameters
      ): ListenableWorker = HogeWorker(repository, appContext, workerParameters)
  }
}

よってDaggerからWorkerFactoryに注入するMapは、具体的なWorkerのClassとそのFactoryとを紐づけたもの(例えば、Map<Class<out ListenableWorker>, @JvmSuppressWildcards Provider<Factory>>)になる。このMapを作るためのBinderは次のような感じ。

@Binds
@IntoMap
@WorkerKey(HogeWorker::class)
fun bindHogeWorker(worker: HogeWorker.Factory): HogeWorkerFactory.Factory

CoroutineLiveDataをどうにかしてテストする

関数androidx.lifecycle.livedataからCoroutineLiveDataというLiveDataを取得できる(CoroutineLiveDatainternalクラスなので実際見えるのはLiveData )。例えば次のような感じで中断関数を渡してやるとライフサイクルとかとの関係をいい感じにやってくれつつemitSource()で渡したLiveDataをセットしてくれる(普通の値の場合はemit()を使う)。

val itemsSource: LiveData<List<Item>> = livedata {
  val source: LiveData<List<Item>> = dao.getItemsSource()
  emitSource(source)
  val items: List<Item> = restApi.fetchItems()
  dao.insert(items)
}

今回はこのLiveDataの値(=中断関数のロジック)に関するテストを書くことを考える。

書いたコードなど

関係ありそうな依存関係:

    implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.3.61"
    implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.3"
    implementation "androidx.lifecycle:lifecycle-livedata-core-ktx:2.2.0"
    implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.2.0"

    testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.3.3'
    testImplementation "androidx.arch.core:core-testing:2.1.0"
    testImplementation 'org.robolectric:robolectric:4.3.1'

なにをやっているか

  • CoroutineLiveDataは内部でLooperを使うらしいのでJVMテストではこれをモックしなければならない。そのためにRobolectricが必要。
  • LiveDataをテストでobserveするためにはInstantTaskExecutorRuleが必要なのでarch.core-testingパッケージを入れている。
  • テストのスレッドとテスト対象のスレッドとを同じにしたいのでテストのDispatcherを注入する。TestCoroutineDispatcherというやつがあるのでこれを使う。テスト対象ではこのDispatcherlivedata()関数に渡す。
  • CoroutineLiveDataをobserveしたあとでRobolectricのshadowOf(getMainLooper()).idle()を呼べば全部の処理が流れるので、テストコードでアサーションできるようになる。めでたし。
  • と思ったけど、テスト対象側(livedata()の中)で起動したコルーチンで起きた例外をテスト側では検知できない。なのでDispatcherと一緒にCoroutineExceptionHandlerも注入してやる必要がある。今回はライブラリに入っていたTestCoroutineExceptionHandlerを使っている。