こんにちは、モバイルクライアントグループの中島です。 年末少し膝を痛めてしまいランニングを中断していたのですが、そろそろ再開していきたい今日この頃。
さて、今回は 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用の拡張関数です。
詳しくはそちらをご参照ください。
Dagger 2.31 における公式への統合
今年の1月15日、 Dagger v2.31 のアップデートにて Assisted Injection が公式に統合されました。
これにより 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さんの記事が大変参考になりました。
今回つまづいたところ
本題に入っていきます。公式の Assisted Injection へのマイグレーションを行なう上で、つまづいた点が二箇所ほどありました。
- WorkManager のビルドが通らない
- 同じ型の Assisted パラメータが判別できない
順を追って説明していきます。
WorkManager のビルドが通らない
Studyplus Android では一部のバックグラウンド処理に WorkManager を利用しています。 WorkManager の Assisted Inject について詳しくは以前の記事をご覧ください。
問題
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の対応なども積極的に行なわれているので、これからもより便利になっていくだろうと思っています。
最後までご覧いただき、ありがとうございました!