Studyplus Engineering Blog

スタディプラスの開発者が発信するブログ

Dagger の Assisted Inject 統合とマイグレーション

こんにちは、モバイルクライアントグループの中島です。 年末少し膝を痛めてしまいランニングを中断していたのですが、そろそろ再開していきたい今日この頃。

さて、今回は Dagger が Assisted Inject を統合したことによるマイグレーションについてお話ししたいと思います。 特に、WorkManager と、2/9にリリースされた v2.32 の変更部分とでつまづいたことについてお話したいと思います。

Assisted Inject とは

簡単に言えば、Daggerによる Inject constructor に対して、 Dagger によってinjectできるもの以外のパラメータを入れ込むための機能です。 Androidで一般的な使い所としては、固有のデータIDを用いて詳細を表示する画面などで、そのIDを不変な値としてViewModelのconstructorに入れたいときなどでしょう。

square/AssistedInject

この機能は以前より外部サポートライブラリである square/AssistedInject を用いて実現されていました。

compileOnly `com.squareup.inject:assisted-inject-annotations-dagger2:0.6.0`
kapt `com.squareup.inject:assisted-inject-processor-dagger2:0.6.0`
@Module(includes = [PresenterModule::class])
abstract class HogeModule {
    // ViewのInjector定義など
}

@AssistedModule
@Module(includes = [AssistedInject_PresenterModule::class])
internal abstract class PresenterModule
// ViewModel
class HogeDetailViewModel @AssistedInject constructor(
    @Assisted private val hogeId: String,
    private val repository: HogeRepository,
) : ViewModel() {

    @AssistedInject.Factory
    interface Factory {
        fun create(hogeId: String): HogeDetailViewModel
    }
}
// View
    private val args: HogeDetailFragmentArgs by navArgs()

    @Inject
    lateinit var viewModelFactory: HogeDetailViewModel.Factory
    private val viewModel: HogeDetailViewModel by assistedViewModels {
        viewModelFactory.create(hogeId = args.hogeId)
    }

なお、assistedVieModels はtakahiromさんの以下の記事を参考にさせていただき作成して運用している、Dagger用の拡張関数です。 詳しくはそちらをご参照ください。

qiita.com

Dagger 2.31 における公式への統合

今年の1月15日、 Dagger v2.31 のアップデートにて Assisted Injection が公式に統合されました。

github.com

これにより square/AssistedInject の依存が消え、PresenterModule など、 Module への追加記述も必要なくなりました。

// ViewModel
class HogeDetailViewModel @AssistedInject constructor(
    @Assisted private val hogeId: String,
    private val repository: HogeRepository,
) : ViewModel() {

    @AssistedFactory <- // ここだけアノテーション名が違います
    interface Factory {
        fun create(hogeId: String): HogeDetailViewModel
    }
}
// View
    private val args: HogeDetailFragmentArgs by navArgs()

    @Inject
    lateinit var viewModelFactory: HogeDetailViewModel.Factory
    private val viewModel: HogeDetailViewModel by assistedViewModels {
        viewModelFactory.create(hogeId = args.hogeId)
    }

このマイグレーションについてもtakahiromさんの記事が大変参考になりました。

qiita.com

今回つまづいたところ

本題に入っていきます。公式の Assisted Injection へのマイグレーションを行なう上で、つまづいた点が二箇所ほどありました。

  • WorkManager のビルドが通らない
  • 同じ型の Assisted パラメータが判別できない

順を追って説明していきます。

WorkManager のビルドが通らない

Studyplus Android では一部のバックグラウンド処理に WorkManager を利用しています。 WorkManager の Assisted Inject について詳しくは以前の記事をご覧ください。

tech.studyplus.co.jp

問題

ViewModelと同様にマイグレーションを行なっていたところ、ビルドエラーが発生しました。

interface ChildWorkerFactory {
    fun create(appContext: Context, params: WorkerParameters): ListenableWorker
}

class HogeWorker @AssistedInject constructor(
    @Assisted private val appContext: Context,
    @Assisted private val params: WorkerParameters,
    private val repository: HogeRepository,
) : CoroutineWorker(appContext, params) {

    override fun doWork(): Result {
        // ~~
    }

    @AssistedFactory // <- アノテーションの変更
    interface Factory : ChildWorkerFactory
}
エラー: [~~.ChildWorkerFactory.create(android.content.Context, androidx.work.WorkerParameters)]
Invalid return type: androidx.work.ListenableWorker. An assisted factory's abstract method must return a type with an @AssistedInject-annotated constructor.

該当する Dagger の生成コードを見てみます。

    @dagger.assisted.AssistedFactory()
    public static abstract interface Factory extends ~~.ChildWorkerFactory {
    }

要は何もoverrideされていないので、継承元である ChildWorkerFactory のままListenableWorkerをcreateしようとしているようです。 その結果、「@AssistedInjectのアノテーションが付与されているconstructorがないぞ」と言われているわけですね。

それならばとcreateメソッドまでoverrideして、返り値の型を「@AssistedInjectのアノテーションが付与されているconstructorを持つWorker」にしてみました。

    @AssistedFactory
    interface Factory : ChildWorkerFactory {
        override fun create(appContext: Context, params: WorkerParameters): HogeWorker
    }

その結果、またビルドエラーが出ましたがメッセージが変わりました。

エラー: 不適合な型:
 dagger.internal.Factory<HogeWorker_Factory_Impl>をProvider<~~.HogeWorker.Factory>に変換できません:

…?

FactoryをProviderに変換できない? それはそうだろうと思うのですが、そこが変わるような変更を加えた覚えがなく、色々いじっても解決することなく結局合計2日程度ここで止まってしまいました。

解決

2月9日、 Dagger v2.32 のアップデートにて解決しました。 リリースノートを見ると、 Java 7で起きる型推論issueだったようです。

余談ですが、試しにDagger v2.31.2 へ戻した上でWorkManagerのあるモジュールをJava 8でビルドしてみたところ、2日間悩んでいたのが嘘のように通りました。 Dagger 関連のコードではなく、gradleでJavaバージョンを変更することで通るようになるとは発想が至りませんでした…修行不足です。

// build.gradle(:workmanager)
compileOptions {
    sourceCompatibility JavaVersion.VERSION_1_8
    targetCompatibility JavaVersion.VERSION_1_8
}

v2.32 で修正されたので「DaggerのためにJava 8でビルドする」ということはもう必要ないとは思いますが、デフォルトでJava 8になるのはいつかなぁなどとふと思いを馳せました。

同じ型の Assisted パラメータが判別できない

問題

v2.32 にすることでWorkManagerの問題は解決したのですが、また新たな壁が立ちはだかりました。 同じ型のパラメータがAssisted Injectされているとエラーメッセージが出る問題です。

エラー:
 @AssistedInject constructor has duplicate @Assisted type: @Assisted java.lang.String
解決

これについてはv2.32のリリースノートを見てすぐ解決しました。

Parameters in @AssistedFactory classes that have the same type now require a name to be set via @Assisted("foo") to disambiguate between arguments. Previously, order of parameters was used.

今までは記述された順番でパラメータの対応をしていたけど、v2.32 からは @Assisted("foo") でそれぞれに名前を設定してcreateメソッドと対応させるよ、ということですね。 やることとしては、同じ型をAssisted Injectしている箇所全てに名前を付けていくだけでした。

// ViewModel
class HogeDetailViewModel @AssistedInject constructor(
    @Assisted("hogeId") private val hogeId: String,
    @Assisted("fugaId") private val fugaId: String,
    private val repository: HogeRepository,
) : ViewModel() {

    @AssistedFactory
    interface Factory {
        fun create(
            @Assisted("hogeId") hogeId: String,
            @Assisted("fugaId") fugaId: String,
        ): HogeDetailViewModel
    }
}

アノテーションへの値の設定もですが、createメソッドのパラメータにも@Assistedアノテーションが必要になったのは新しい要素ですね。 以前のsquare/AssistedInject では引数の名前そのものの一致で対応させていたので、自由度は増えたけど少し冗長かなという印象は否めない感じでしょうか。

終わりに

簡単ではありますが、Studyplus Androidにおける、Dagger の公式Assisted Inject対応の際に引っかかった事例を紹介しました。

Assisted Injectは、Studyplus Androidにとって必須の機能として重宝しているので、 Dagger に統合されたのは非常に嬉しいですね。 square/AssistedInject の完成度も高かったためかどうしても比較してしまう部分もあります。 ですが、issueの対応なども積極的に行なわれているので、これからもより便利になっていくだろうと思っています。

最後までご覧いただき、ありがとうございました!