Jetpack ComposeでHiltを使ってDIする

# はじめに
私は業務でServer-side Kotlinの開発に携わっているのですが、Androidアプリ開発に関してはかなり初心者です。
ふと、Jetpack Composeでアプリを作ってみようと思い立ち、その過程で勉強したDIライブラリの[Hilt](https://dagger.dev/hilt/)の使い方についてまとめました。

# 開発環境
- PC: M1 Mac mini / macOS 12.5
- Android Studio: Dolphin|2021.3.1

# サンプルアプリの概要
## 機能要件
- [Joe Schmoe](https://joeschmoe.io/)のAPIをコールして、アバターを生成
- アバターをローカルDBに保存
- 保存されているデータの一覧を表示

Joe Schmoe APIを使用したのは無料で利用できるAPIを探していてたまたま見つけたからです。
勉強のためとは言え、無料で利用させていただき、感謝いたします。

## ライブラリ
- DI: Hilt
- その他: 画像表示にCoil、DatabaseにRoomを使用しています。

build.gradleの設定については[Andoridの公式ページ](https://developer.android.com/training/dependency-injection/hilt-android?hl=ja#component-default)を参照してください。

## 設計
基本的にはクリーンアーキテクチャを採用しています。
Android開発ではMVVMとRepositoryパターンがよく用いられると思いますが、複数のレイヤーに対してDIするやり方を学ぶために敢えてViewModelからUseCaseを呼び出すようにしています。

### ディレクトリ構成

```
app
├── di
│   └── modules
├── domains
│   ├── entities
│   └── repositories
├── infrastructures
│   ├── dao
│   └── repositories
├── presentations
│   ├── navigations
│   ├── screens
│   ├── theme
│   ├── viewmodels
│   └── MainActivity.kt
├── usecases
│       ├── inputports
│       └── interactors
└── MainApplication.kt
```
拡張子が付いてるもの以外はディレクトリを指してます。

# Hiltの使い方
## 準備
### MainApplicationクラスの作成
Jetpack Composeプロジェクトを開始しただけではまだ作成されていないので、MainApplicationクラスを追加します。
特に詳細の実装は不要です。

```kotlin

@HiltAndroidApp
class MainApplication: Application()

```

### MainActivityの修正
既存のMainActivityに`@AndroidEntryPoint`を付けるだけです。

```kotlin
@AndroidEntryPoint
class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
    }
}
```

### AndroidManifest.xmlの修正
`<application>`タグに作成したMainApplicationのパスを設定します。

```kotlin

    <application
        ~
        ~
        android:name=".MainApplication">
        <activity
            android:name=".presentations.MainActivity"
        ~
        ~
    </application>

```
## 実装
### ViewにViewModelをインジェクト
Jetpack ComposeではViewModelを使用することは推奨されていない[^1]ようですが、HiltはViewModelをライフサイクルに合わせて制御してくれるので安全に使用できると思っています（詳細は[公式ページ](https://dagger.dev/hilt/components)を参照）。

ここでは、ViewModelにUseCaseをインジェクトしています。
ViewModelは`@HiltViewModel`アノテーションを付けると特に何も記述しないで自動でバインディングしてくれます。


```kotlin
@HiltViewModel
class AvatarCreationScreenViewModel @Inject constructor(
    private val saveAvatarUseCase: SaveAvatarUseCase,
): ViewModel() {
    // View Model Logic
}
```

一方で、ViewではViewModelを`hiltViewModel()`から取得します。

```kotlin
@Composable
fun AvatarCreationScreen(
    viewModel: AvatarCreationScreenViewModel = hiltViewModel(),
    navigateToList: () -> Unit
) {
    // View
}
```

ViewModelは裏でインジェクトされるため、NavigationGraphではViewModelを敢えて渡さなくて良いのが楽です。

```kotlin
@Composable
fun NavigationGraph(navController: NavHostController) {
    NavHost(
        navController = navController,
        startDestination = "avatar_creation"
    ) {
        composable("avatar_creation") {
            AvatarCreationScreen(navigateToList = {
                navController.navigate("avatar_list")
            })
        }
        composable("avatar_list") {
            AvatarListScreen(backToCreation = {
                navController.popBackStack()
            })
        }
    }
}
```

### UseCaseとReppositoryをインジェクト
ViewModel以外をインジェクトする場合は、`di/module`パスにModuleクラスを定義する必要があります（[公式ページ](https://dagger.dev/hilt/modules)）。
ネットを探すと書き方に揺らぎがありますが、公式では`provideXXXX()`に統一されてるので`provideXXXX()`を使えば良いです。

ざっと要点をまとめると
- `di/module`パスに定義すると自動で読み込まれる
- `@InstallIn`でコンポーネントのスコープを設定する
    - ここでは、UseCaseやRepositoryはViewのライフサイクルには依存してほしくないので、`SingletonComponent`にしています
- `@Provides`でバインドする対象を定義する
- コンテキストを渡す場合は`@ApplicationContext`を利用するとApplicationのコンテキストがインジェクトされます
    - Roomを使うために必要だったからです
    - `@ActivityContext`など、コンポーネントのスコープに合わせる必要があります

```kotlin
@Module
@InstallIn(SingletonComponent::class)
object ApplicationModule {

    @Provides
    fun provideAvatarRepository(
        @ApplicationContext context: Context
    ): AvatarRepository = AvatarRepositoryImpl(
        context = context
    )

    @Provides
    fun provideListAvatarUseCase(
        avatarRepository: AvatarRepository
    ): ListAvatarUseCase = ListAvatarUseCaseImpl(
        repository = avatarRepository
    )

    @Provides
    fun provideSaveAvatarUseCase(
        avatarRepository: AvatarRepository
    ): SaveAvatarUseCase = SaveAvatarUseCaseImpl(
        repository = avatarRepository
    )
}
```
# 最後に
ソースコードは[こちら](https://github.com/ohsugi5532/android_practice/tree/main/HiltApplicationSample)に置いてます。雑な設計なので参考程度に。

# 参考文献
[^1]: https://qiita.com/karamage/items/9b2b5a79c364b72836d4