Dagger Hilt ことはじめ

先日 Hilt のアルファリリースとドキュメントが公開されました。 最近 Dagger を触っている身としてはどうしても気なり、dagger.android を利用している Android プロジェクトで Hilt への移行を試しました。

今回の環境は 2020/6/16 時点で hilt-android(2.28-alpha) および androidx.hilt(1.0.0-alpha01) を使用しました。

dagger.android からのマイグレーション

アルファリリースが行われた時点ですでに豊富なドキュメントが公開されています。 dagger.android からのマイグレーションにおいては公式から以下のドキュメントが公開されています。

dagger.dev

自分は参考にしませんでしたが、CodeLab も公開されています。

codelabs.developers.google.com

今回は具体的な移行手順の説明ではなく、移行する上でのポイントとつまずいた内容をまとめてみました。

移行する上でのポイント

これまで自身で実装する必要があった多くのものを Hilt が提供してくれるようになります。 特に Component と Activity や Fragment ごとに作成していた SubComponent を(難しいことをしなければ)実装する必要がなくなりました。 これにより多くの実装が減り、構造が少し変化しています。

@HiltAndroidApp @AndroidEntryPoint の登場

Hilt では Application に @HiltAndroidApp を付与するだけで、提供される Component の仕組みにのることができます。 これまでは Component の作成をせっせとやっていましたが、複雑な Component を作る必要がなければこれだけで完結します。

Activity や Fragment での SubComponent の作成も @AndroidEntryPoint のみですべてが解決するようになりました。 AndroidInjection#inject@ContributesAndroidInjectorHasAndroidInjector の実装がすべて必要なくなりました。

仕組みとしてはアノテーションプロセッサによって SubComponent の生成や依存注入の実装を持った中間クラスを生成し、継承関係に滑りこませるようなことをしているようです。

Component と Module の関係の変化

これまでは自作した Component から Module を指定し、依存グラフを構築するという以下のような関係で実装をしていました。

f:id:ksfee:20200616035058p:plain
Component が参照する Module を指定

Hilt では Component を基本的に自作する必要がなく、提供されるものを利用します。 しかし提供される Component は開発者が直接扱うことはできず、これまでのように Module を指定するということができません。

そのためこれまでとは逆に Hilt では Module 自身が所属する Component を @InstallIn アノテーションによって指定するようになりました。

@InstallIn(ApplicationComponent.class)
@Module
interface NetworkModule {
    ...
}

その結果 includes で Module をまとめていたいわゆる Aggregation Module のような存在が必要なくなり、以前より Module がフラットに並ぶような構造となりました。

f:id:ksfee:20200616035436p:plain
Module が Component を指定するように

Module の @InstallIn アノテーションによる Component の指定は必須となっており、存在しない場合コンパイル時にエラーとなります。 どうしても includes を利用して自身では Component の指定を行いたくない場合は、@DisableInstallInCheck アノテーションによって回避可能になっています。 他にもオプションによってコンパイル時のチェック自体をやめることも可能です、詳しくはドキュメントを参照してください。

Hilt では ApplicationComponent のように提供される Component がいくつか存在します。 こちらも詳しくは公式ドキュメントを参考にしてください。

Component 初期化時の引数

これまで自作していた Component では BuilderFactory 内で値を挿入し、依存グラフの中で参照することができました。 この機能も提供される Component を直接扱えない関係上、作成時に値を付与することができません。

主な代替手段としては Module に置き換えて依存グラフ上で参照可能にすることです。 Application や Context(ApplicationContext) などは提供される Component ごとに Hilt がバインディングしてくれる値があるので、そちらを利用することになります。

一応カスタム Component をバインディングする値を増やすことはできるので、どうしてもという場合はドキュメントを参考に実装してみてください。

@ViewModelInject の導入

ViewModel では Jetpack integration として提供されている @ViewModelInject を利用します。 AssistedInject を踏襲するようなインターフェイスとなっており、ViewModelProvider を自作する必要がなくなり、 KTXviewmodels を利用するだけで簡単に依存注入を行えるようになりました。

Android developers にドキュメントが公開されているので詳細はそちらを参照してください。

つまずいた個所

Activity, Fragment の実体をどう扱うか

dagger.android では @ContributesAndroidInjector によって SubComponent を生成し、その SubComponent では値を注入する対象であるクラスをそのまま扱うことができました。

Hilt では各 Component ごとにデフォルトでバインディングされた値を android.app.Activityandroidx.fragment.app.Fragment として依存グラフで扱うことができますが、具体的なクラスとして扱うことができません。

具体的には以下のような問題が発生すると考えています。

// Activity
class SampleActivity : AppCompatActivity(), ViewInterface {}

// Module
@InstallIn(ActivityComponent::class)
interface SampleModule {
    @Binds
    fun bindView(view: SampleActivity): ViewInterface  // SampleActivity が依存グラフに存在しない
}

解決するための手段はいくつか考えましたが、Hilt の仕組みから外れず実装コストを最小減に抑えられる方法としてキャストを利用するようにしてみました。

@InstallIn(ActivityComponent::class)
object SampleModule {
    @Provides
    fun provideView(activity: Activity): ViewInterface = activity as SampleActivity
}

ただ少し強引すぎる手段なので、より良い方法を模索中です。

Bundle の値を ViewModel で受け取りたい

Hilt では ViewModel は ActivityRetainedComponent 下で依存注入が行われますが、現時点では Activity, Fragment と一切関係を持てないため Bundle から値を抽出して受け渡すというようなことができません。

AssistedInject の機能を搭載する予定があるようなので、そちらに期待しています。 現状の仕様ではほぼ解決することができないように思えるので、おとなしく機能拡充を待つことになりそうです。

medium.com

アノテーションプロセッサが静かに失敗する

kapt のタイミングでエラーが発生しても kapt の Gradle タスクがエラーをうまく出せず、タスクが正常終了してしまう問題に遭遇しました。

この問題は Gradle に --debug オプションを渡し、吐き出されるログを見続けることで、依存が足りずエラーが出ている個所を発見し解決しました。 具体的には ViewModel integration の依存(androidx.hilt:hilt-lifecycle-viewmodel)が Application モジュールに存在していないことが問題でした、ぜひ気を付けてください。

CustomView

dagger.android では依存注入のエントリーポイントは Activity, Fragment, Service, BroadcastReceiver を対象としていましたが、Hilt では Custom View 向けにもインターフェイスが用意されています。

しかし現行のアルファバージョンでは利用している JavaPoet 側に問題があり、@AndroidEntryPoint によって生成されるクラスで Nullable, Nonnull アノテーションが壊れてしまう問題に遭遇しました。 com.google.dagger:hilt-android から参照している com.square.javapoet:1.11.1 では問題ないようですが、androidx.hilt:hilt-compiler が参照している 12.1 への依存に引っ張られて問題が発生しているようです。

github.com

継承関係を持つ Fragment でそれぞれ AndroidEntryPoint を指定できない

アノテーションプロセッサで生成されるクラスを継承関係に挟み込む Hilt の仕組みが影響している問題です。 手元で発生を確認したのは Fragment ですが、AndroidEntryPoint をもつクラス同士で継承関係があれば Activity などでも発生するものだと思います。 継承関係手を加えることで似たような問題がほかにも出てきそうな予感がします。

github.com

テストで Hilt を利用する場合 @HiltAndroidTest が必須に

テスト実行時に Hilt で依存注入される値の切り替えなどを行う場合、HiltTestApplication または @CustomTestApplication を利用した Application クラスに TestApplication を切り替える必要があります。 これに加えてテストクラスに @HiltAndroidTest アノテーションをつけ、@BindValue によって注入される値の切り替えを行うことができます。

しかし Hilt を利用する TestApplication を指定した場合、@HiltAndroidTest がついていないテストクラスはすべて実行時エラーとなってしまいます。 つまり Hilt を利用していないクラスのテストクラスでも、同じ Gradle モジュールで RobolectricTest または AndroidTest として実装するものはすべてアノテーションの付与が必須となっています。

これが仕様上の限界なのか意図していないバグのようなものなのかはまだわかっていませんが、このままでは簡単に移行することは難しい印象です。 @BindValue などは非常に便利なので上手い着地点に落ち着くことを祈っています。

まとめ

アルファリリースを検証する上でいくつかの問題に遭遇しましたが、個人的には ViewModel の問題に対して良い解決策が出てくるまでは移行には早いと感じました。

また継承関係に割り込む仕様は少し強引なので、今後も問題がぽろぽろ出てくるかなと予想しています。

ただ Hilt は実装コストや学習コストを下げる上で大きな希望となってくれるものと期待しているので、今後も変更を追っていきたいと思います。