May 30, 2020

Dagger Android Hiltが神

Androidでよく使われているDIライブラリであるDaggerは今まで「分かりづらい」「学習コストが高い」「難しい」という声が多くありました。

元々Dagger はSquare社が開発したもので、歴史的経緯があり今ではGoogleがDaggerをforkして通称Dagger2 を作っています。
6年ぐらい前からAndoridでもDagger2を使うのが主流になってきたものの、Android独自のライフサイクルとDIのライフサイクルを組み合わせるためのボイラープレートが多く、Annotationによるコード生成から動作イメージを掴みづらいのも相まって初学者を苦しめる要因となっていました。
そこで開発されたのが、Dagger Android Supportです。Dagger Andorid SupportはDagger2のプラグインとして提供されていて、Andoridのライフサイクルの記述をサポートしてくれるのですが、根本的にAndoridのApplicationライフサイクルに対してのDIを解決しているわけではなく、あくまでサポートという形なため、楽にはなるのですが複雑さを解決するにはイマイチ物足りなさがありました。さらに、昨今のマルチモジュールの流れと相性が悪く、マルチモジュール構成ではDagger Android Supportを使わない方が逆にシンプルになる状況もありました。

これらの問題を解決すべく、昨年のAndroid Dev Summit 2019にてDagger Hiltの計画が発表され、ついに2020年5月にalphaバージョンがリリースされました。

Hiltの名の通り、今までDagger初心者には難しかった初期設定のほとんどをライブラリ側が吸収してくれ、アプリケーション開発者は依存の注入のみに集中することが可能になりました。
私自身もDagger歴5,6年になりますが、正直何度やってもハマる時はめちゃくちゃハマる事が多く大好きなライブラリでもありますが、初期設定の時は嫌いなライブラリでもありましたw
まだalpha版が出たばかりでドキュメントが全く無く、javadocの記述しか無いため、使用方法が間違っている箇所があるかもしれません。気付き次第変更する予定です。

Maven repositoryはこちら になります。 まだありませんが、ドキュメントの建設予定地はおそらくこちら です。

導入手順

Gradle

buildscript {
    dependencies {
       classpath 'com.google.dagger:hilt-android-gradle-plugin:2.28-alpha'
    }
}
apply plugin: 'dagger.hilt.android.plugin'
apply plugin: 'kotlin-kapt'

dependencies {
    implementation 'com.google.dagger:hilt-android:2.28-alpha'
    kapt 'com.google.dagger:hilt-android-compiler:2.28-alpha'
}

Application class

まずはApplicationクラスを用意します。
今回はApplicationクラスに @HiltAndroidApp annotationをつけるだけです。 今まではApplication用のComponentクラスを自分で定義する必要がありましたが、HiltではDagger側が用意してくれます。
さらに、Hiltでは何も考えずにAndorid自体のApplicationクラスを継承できます。 今までは DaggerApplication を継承しない場合は、HasAndroidInjectorを自分で実装する必要がありました。

Hiltからは @HiltAndroidApp のannotationを付けるだけで生成されたComponentの読み込みとInjectorの実装を自動生成してくれます。

@HiltAndroidApp
class App : Application() {
}

Module

Hiltでは

  • Application
  • Activity
  • ActivityRetained
  • Fragment
  • Service
  • View
  • ViewWithFragment

のライフサイクルが予め定義されています。

これにより今までは、自分で管理しなければならなかったライフサイクルをDagger側で管理してくれます。 Moduleにこのライフサイクルを定義するには、@InstallIn annotationを使います。
今回のサンプルでは、Application, Activity, Fragmentで動作するモジュールを定義します。 従来と同じく、@Provides annotationで注入したいものを定義します。

@Module
@InstallIn(ApplicationComponent::class)
class ApplicationModule {

  @AppHash
  @Provides
  fun provideHash(): String {
      return hashCode().toString()
  }
}
@Module
@InstallIn(ActivityComponent::class)
class ActivityModule {

    @ActivityHash
    @Provides
    fun provideHash(): String {
        return hashCode().toString()
    }
}
@Module
@InstallIn(FragmentComponent::class)
class FragmentModule {

    @FragmentHash
    @Provides
    fun provideHash(): String {
        return hashCode().toString()
    }
}

今回は同じ型のインスタンスを定義しているので、Dagger側がそれぞれを識別出来るようにQualifierを定義しています。

@Qualifier
@Retention(AnnotationRetention.RUNTIME)
internal annotation class AppHash

@Qualifier
@Retention(AnnotationRetention.RUNTIME)
internal annotation class ActivityHash

@Qualifier
@Retention(AnnotationRetention.RUNTIME)
internal annotation class FragmentHash

Qualifierに関しては今までと全く同じです。

なんとこれだけで、既にProvider側は完了しています!!!
Hiltすごい

Activity, Fragment

実際にActivityとFragmentから呼び出してみます。

呼び出しに必要なのものは @AndroidEntryPoint annotationのみです。
今までは、UI毎にModuleを作成して、Android Supportの場合は @ContributesAndroidInjector を使い、それぞれのライフサイクルを自分で定義する必要がありましたが、ライフサイクルに関しては既に上記で@InstallIn annotationにて定義されているため、注入される側からはそれぞれのProviderを呼び出すだけでInjectされます。
本当に素晴らしい👏

今回のサンプルではこのようにな構成になっています。

MainActivityがあり、その上にFirstFragment, SecondFragmentがそれぞれ乗っています。それとは別にSecondActivityが用意されていて、MainActivityから遷移できます。
ViewModelに関してはこのセクションでは触れません。後述します。

@AndroidEntryPoint
class MainActivity : AppCompatActivity(R.layout.activity_main) {

    @AppHash
    @Inject
    lateinit var appHash: String

    @ActivityHash
    @Inject
    lateinit var activityHash: String

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        Log.v("main activity", "app hash: $appHash")
        Log.v("main activity", "activity hash: $activityHash")
    }
}
@AndroidEntryPoint
class MainFirstFragment : Fragment(R.layout.fragment_first) {

    @AppHash
    @Inject
    lateinit var appHash: String

    @ActivityHash
    @Inject
    lateinit var activityHash: String

    @FragmentHash
    @Inject
    lateinit var fragmentHash: String

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        Log.d("first fragment", "app hash: $appHash")
        Log.d("first fragment", "activity hash: $activityHash")
        Log.d("first fragment", "fragment hash: $fragmentHash")
    }
}
@AndroidEntryPoint
class MainSecondFragment : Fragment(R.layout.fragment_second) {

    @AppHash
    @Inject
    lateinit var appHash: String

    @ActivityHash
    @Inject
    lateinit var activityHash: String

    @FragmentHash
    @Inject
    lateinit var fragmentHash: String

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        Log.i("second fragment", "app hash: $appHash")
        Log.i("second fragment", "activity hash: $activityHash")
        Log.i("second fragment", "fragment hash: $fragmentHash")
    }
}

ちなみにvarでnullableにする場合は @JvmField が必要です。

@JvmField
@Model
@Inject
var hashCode: String? = null

今回のようにlateinitを使う場合はJvmFieldは不要です。

@Model
@Inject
lateinit var hashCode: String

実行結果はこのようになります。

V/main activity: app hash: 493167393
V/main activity: activity hash: 558510918

D/first fragment: app hash: 493167393
D/first fragment: activity hash: 558510918
D/first fragment: fragment hash: 362011381

I/second fragment: app hash: 493167393
I/second fragment: activity hash: 558510918
I/second fragment: fragment hash: 604460515

W/second activity: app hash: 493167393
W/second activity: activity hash: 850460303

Application, Activity, Fragment それぞれのライフサイクルによってインスタンスが異なるのがわかると思います。
今までより圧倒的に簡単にライフサイクルの管理ができています👏 神か

View model

Androidではもう一つ厄介なViewModelのDI問題があります。
これも今までは、自分でViewModelのライフサイクルを記述してDIする必要があり、さらに最近追加されたSavedStateHandle をDIするにはDaggerを完全理解していないと結構難しかったように思います。
なんと、Dagger Hiltではこの問題も解決されています。神か

ViewModelのDIはDagger側ではなく、androidxのプラグインとして用意されています。
2020年5月現在はSNAPSHOTです。SNAPSHOTは複数上がっているので執筆時最新のbuild番号 6543454 を使います。

Gradle

SNAPSHOTを使うので、rootのGradleにMavenリポジトリのURLを設定して、appのGradleファイルにライブラリを追加します。

allprojects {
    repositories {
        maven {
            url "https://androidx.dev/snapshots/builds/6543454/artifacts/repository/"
        }
    }
}
dependencies {
    implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0'
    implementation 'androidx.lifecycle:lifecycle-viewmodel-savedstate:2.2.0'

    implementation 'androidx.hilt:hilt-common:1.0.0-SNAPSHOT'
    implementation 'androidx.hilt:hilt-lifecycle-viewmodel:1.0.0-SNAPSHOT'
    kapt 'androidx.hilt:hilt-compiler:1.0.0-SNAPSHOT'
}

Singleton

InstallInの箇所では説明しませんでしたが、もちろん@Singleton も利用可能です。

ViewModel側から呼び出すSingletonのRepositoryを定義します。
今まで通りInject constructorが利用可能なため、Repository自体にAPI ClientなどもDI可能です。

@Singleton
class SampleRepository @Inject constructor() {
  fun getSomething(): Something
}

ViewModel

なんとこれだけです。
アプリケーション開発者側はライフサイクルの定義は何も要りません。
一度でも自分でDaggerを使ったViewModelのDIをしたことがあるなら感動すると思います。
SavedStateHandle@Assisted annotationを付けるだけです。神か

class MainViewModel @ViewModelInject constructor(
    private val repository: SampleRepository,
    @Assisted private val savedState: SavedStateHandle
) : ViewModel() {

    fun getRepositoryHash(): String = repository.toString()
}

Activity, Fragment

ActivityとFragment側はktxで用意されている activityViewModelsviewModels を使えば取得可能です。

@AndroidEntryPoint
class MainActivity : AppCompatActivity(R.layout.activity_main) {

    private val viewModel by viewModels<MainViewModel>()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        Log.v("main activity", "repository hash: $repository")
        Log.v("main activity", "activity view model: $viewModel")
        Log.v("main activity", "activity vm repository: ${viewModel.getRepositoryHash()}")
    }
}
@AndroidEntryPoint
class MainFirstFragment : Fragment(R.layout.fragment_first) {

    private val activityViewModel by activityViewModels<MainViewModel>()
    private val fragmentViewModel by viewModels<MainViewModel>()

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        Log.d("first fragment", "activity view model: $activityViewModel")
        Log.d("first fragment", "fragment view model: $fragmentViewModel")
        Log.d("first fragment", "activity vm repository: ${activityViewModel.getRepositoryHash()}")
        Log.d("first fragment", "fragment vm repository: ${fragmentViewModel.getRepositoryHash()}")
    }
}

SecondFragmentにもFirstFragmentと同様にDIしたものを実行するとこうなります。

V/main activity: activity vm repository: SampleRepository@f1e9400
V/main activity: activity view model: MainViewModel@146eca7e

D/first fragment: activity vm repository: SampleRepository@f1e9400
D/first fragment: fragment vm repository: SampleRepository@f1e9400
D/first fragment: activity view model: MainViewModel@146eca7e
D/first fragment: fragment view model: MainViewModel@3e241260

I/second fragment: activity vm repository: SampleRepository@f1e9400
I/second fragment: fragment vm repository: SampleRepository@f1e9400
I/second fragment: activity view model: MainViewModel@146eca7e
I/second fragment: fragment view model: MainViewModel@4608120e

SingletonのRepository, Fragment側から取得したActivityのViewModelは全て同じインスタンスとなっていて、FragmentライフサイクルになっているViewModelはそれぞれ別インスタンスになっています👏

ただannotationを書くだけで良いなんて… 神か

まとめ

今までわかりづらかった箇所が全てシンプルかつ簡単になっています。
簡単すぎて使い方が間違っているかもしれません。
もうAndroid界隈では、Dagger難しいは死語になりました。

Hilt最高です。
神か。

今回のサンプルリポジトリはこちらになります。

© AAkira 2023