DIライブラリ「Hilt」のセットアップ&使い方(Kotlin)

## はじめに

本記事は [Swift/Kotlin愛好会 Advent Calendar 2020](https://qiita.com/advent-calendar/2020/love_swift_kotlin) の21日目の記事です。
空いているので埋めました。

Hiltのセットアップ方法と使い方を紹介します。

## 「Hilt」とは？

Android向けのDIライブラリです。

Daggerの上に構築されており、DaggerコンポーネントやAndroidクラスを自動的に注入するコードを生成するため、Daggerを使うよりボイラープレートが減ります。

## 環境

- OS：macOS Big Sur 11.1
- Android Studio：4.1.2
- Kotlin：1.4.10
- Gradle：6.8
- Gradle plugin：4.1.2
- Hilt：2.31-alpha

## セットアップ

### kaptのセットアップ

Hiltはアノテーションを使ってコードを生成するため、kaptが必要です。
以下の記事を参考にセットアップしてください。
https://qiita.com/uhooi/items/836902cdd322f9accded

### Hiltのインストール

ルート直下の「build.gradle」にプラグインを追加します。

```diff_groovy:/biuld.gradle
buildscript {
    ext {
+         hilt_version = '2.31-alpha'
    }
    repositories {
        google()
        jcenter()
    }
    dependencies {
+         classpath "com.google.dagger:hilt-android-gradle-plugin:$hilt_version"
    }
}
```

appフォルダ配下の「build.gradle」にプラグインの適用と依存関係を追加します。

```diff_groovy:/app/build.gradle
+ apply plugin: 'dagger.hilt.android.plugin'

dependencies {
+     implementation "com.google.dagger:hilt-android:$rootProject.hilt_version"
+     kapt "com.google.dagger:hilt-android-compiler:$rootProject.hilt_version"
}
```

以下はなくても動作しましたが、公式ドキュメントに記載されているので追加しています。
詳しい方がいたら教えていただけると嬉しいです :pray: 

```diff_groovy:/app/build.gradle
android {
+     compileOptions {
+         sourceCompatibility JavaVersion.VERSION_1_8
+         targetCompatibility JavaVersion.VERSION_1_8
+     }
}

+ kapt {
+     correctErrorTypes = true
+ }
```

### アプリケーションクラスの実装

アプリケーションクラスに `@HiltAndroidApp` アノテーションを付けます。
これにより、アプリケーションレベルのコンポーネントがHiltで使えるようになります。

要は「このアプリではHiltを使いますよー」というおまじないです。

```diff_kotlin:UhooiPicBookApp.kt
+ import dagger.hilt.android.HiltAndroidApp

+ @HiltAndroidApp
class UhooiPicBookApp : Application() {
    // ...
}
```

これでHiltのセットアップは完了です。

## 使い方

### ①インターフェースを通してコンストラクタインジェクションする

#### Before

例として、以下のインターフェースとその実装クラスを用意します。

```kotlin:MonstersRepository.kt
interface MonstersRepository {
    // ...
}
```

```kotlin:MonstersFirestoreClient.kt
class MonstersFirestoreClient : MonstersRepository {
    // ...
}
```

上記のインターフェースを通して、以下のビューモデルにDIする方法を紹介します。
現状ではビューモデル内で実装クラスのインスタンスを生成しているため、DIできていません。

```kotlin:MonsterListViewModel.kt
class MonsterListViewModel : ViewModel() {
    private val repository: MonstersRepository = MonstersFirestoreClient() // TODO: DIする
}
```

#### After

ビューモデルのコンストラクタに `@Inject` アノテーションを付け、コンストラクタでインターフェースを型とした引数を用意します。
これにより、Hiltが自動的に対象インターフェースの実装クラスをDIします。

```diff_kotlin:MonsterListViewModel.kt
+ import javax.inject.Inject

- class MonsterListViewModel : ViewModel() {
+ class MonsterListViewModel @Inject constructor(
+     private val repository: MonstersRepository
+ ) : ViewModel() {
-         private val repository: MonstersRepository = MonstersFirestoreClient() // TODO: DIする
}
```

実装クラスのコンストラクタにも `@Inject` アノテーションを付ける必要があります。

```diff_kotlin:MonstersFirestoreClient.kt
+ import javax.inject.Inject

- class MonstersFirestoreClient : MonstersRepository {
+ class MonstersFirestoreClient @Inject constructor() : MonstersRepository {
    // ...
}
```

しかし、インターフェースの実装クラスは複数あるかもしれないので、これだけではHiltがどの実装クラスをDIしていいかわかりません。

そこで用意するのがHiltモジュールです。
Hiltモジュール内で「このインターフェースにはこの実装クラスをDIする」ことを定義し、Hiltにインスタンスの提供方法を伝えます。

Hiltモジュールを新規作成します。

```kotlin:AppModule.kt
package com.theuhooi.uhooipicbook.di

import com.theuhooi.uhooipicbook.modules.monsterlist.MonstersRepository
import com.theuhooi.uhooipicbook.repository.monsters.firebase.MonstersFirestoreClient
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton

@Module
@InstallIn(SingletonComponent::class)
abstract class AppModule {
    @Singleton
    @Binds
    abstract fun bindMonstersRepository(monstersFirestoreClient: MonstersFirestoreClient): MonstersRepository
}
```

コードを上から説明します。

Hiltモジュールであることを表すために `@Module` アノテーションを付けます。

Hiltモジュールの依存関係をどこまで適用するかの範囲を表すため、 `@InstallIn` アノテーションを付けます。
`SingletonComponent` は、Hiltモジュールの全依存関係がアプリの全アクティビティで使えることを表します。
アプリ全体で適用されるHiltモジュールは、ルート直下に `di` パッケージを作成し、その中に `AppModule.kt` という名前で実装するのが一般的なようです。

インターフェースにDIする場合は、Hiltモジュールを抽象クラスで定義します。
インスタンスの提供方法を伝える関数も抽象関数とし、 `@Binds` アノテーションを付けます。
すべての依存関係で1つのインスタンスを使い回すには `@Singleton` アノテーションを付けます。

関数は `bind{インターフェース名}({実装クラス名の先頭を小文字にしたもの}: {実装クラス名}): {インターフェース名}` と定義するのが一般的なようです。
試していないですが、インターフェースと実装クラスさえ指定していれば、関数名と引数名は任意だと思います。

これで「 `MonstersRepository` インターフェースに `MonstersFirestoreClient` クラスのインスタンスをDIする」ことをHiltに伝えられたので、DIに必要な実装が完了です。

### ②自分で所有していないクラスをDIする

まだ実装したことがないため、簡単にのみ説明します。

インターフェースのときとは異なり、Hiltモジュールを `object` で定義し、 インスタンスの提供方法を伝える関数は通常の関数に `@Provides` アノテーションを付けます。
関数内でインスタンスを生成して返します。

```kotlin:AppModule.kt
@Module
@InstallIn(SingletonComponent::class)
object AppModule {
    @Probides
    fun provideGoogleRepository(): GoogleRepository {
        return GoogleDBClient()
    }
```

### ③ビューモデルをフィールドインジェクションする

#### Before

例として、以下のフラグメントに先ほど紹介した `MonsterListViewModel` をフィールドインジェクションする方法を紹介します。

```kotlin:MonsterListFragment.kt
class MonsterListFragment : Fragment() {
    private val viewModel: MonsterListViewModel // TODO: DIする
}
```

```kotlin:MonsterListViewModel.kt
import javax.inject.Inject

class MonsterListViewModel @Inject constructor(
    private val repository: MonstersRepository
) : ViewModel() {
    // ...
}
```

#### After

ビューモデルに `@HiltViewModel` アノテーションを付けます。

```diff_kotlin:MonsterListViewModel.kt
+ import dagger.hilt.android.lifecycle.HiltViewModel
import javax.inject.Inject

+ @HiltViewModel
class MonsterListViewModel @Inject constructor(
    private val repository: MonstersRepository
) : ViewModel() {
    // ...
}
```

フラグメントに `@AndroidEntryPoint` アノテーションを付け、ビューモデルの変数に `by viewModels()` を付ければ、自動的にDIされます。

```diff_kotlin:MonsterListFragment.kt
+ import androidx.fragment.app.viewModels
+ import dagger.hilt.android.AndroidEntryPoint

+ @AndroidEntryPoint
class MonsterListFragment : Fragment() {
-     private val viewModel: MonsterListViewModel // TODO: DIする
+     private val viewModel: MonsterListViewModel by viewModels()
}
```

`@AndroidEntryPoint` アノテーションは依存するクラスにも付ける必要があります。
例えば、フラグメントをにアノテーションを付ける場合は、そのフラグメントを使っているアクティビティ（今回は `MainActivity` とする）にもアノテーションを付ける必要があるということです。

```diff_kotlin:MainActivity.kt
+ import dagger.hilt.android.AndroidEntryPoint

+ @AndroidEntryPoint
class MainActivity : AppCompatActivity() {
    // ...
}
```

`viewModels()` は対象のクラス（今回は `MonsterListFragment` ）のみで生存するようです。
`MonsterListFragment` を使用しているアクティビティ（ `MainActivity` ）でも生存したい場合は `activityViewModels()` とすればいいようです。

## おまけ: チートシート

公式でHiltのチートシートが公開されています。
一通り目を通してから、公式ドキュメントでキャッチアップするのがいいかもしれません。

https://medium.com/androiddevelopers/hilt-and-dagger-annotations-cheat-sheet-9adea070e495

## おわりに

少ないコード量でDIを実現できました！
Hiltにはできることがまだまだたくさんあるので、必要に応じて勉強していきたいです。

以上、 [Swift/Kotlin愛好会 Advent Calendar 2020](https://qiita.com/advent-calendar/2020/love_swift_kotlin) の21日目の記事でした。

## 参考リンク

- [Hilt を使用した依存関係の注入  |  Android デベロッパー  |  Android Developers](https://developer.android.com/training/dependency-injection/hilt-android?hl=ja)
- [Hilt](https://dagger.dev/hilt/)
- [Release Dagger 2.31 · google/dagger](https://github.com/google/dagger/releases/tag/dagger-2.31)
- [Use Dagger Hilt · Issue #71 · uhooi/UhooiPicBook-Android](https://github.com/uhooi/UhooiPicBook-Android/issues/71)
- [dagger_hiltの導入と実行エラーの解除 by bassga · Pull Request #74 · uhooi/UhooiPicBook-Android](https://github.com/uhooi/UhooiPicBook-Android/pull/74)
- https://twitter.com/AndroidDev/status/1443742458647617542?s=20