【Android】Hiltを使用してRoomのCodelabソースコードにDI風にリファクタリングした話

# はじめに

皆さん、ごきげんよう！れぶです！

今回の記事では、公式チュートリアルCodelabの「[Android Room with a View - Kotlin](https://developer.android.com/codelabs/android-room-with-a-view-kotlin#0)」で扱うコードを`Hilt`でDIしていきます。

上記のCodelabでは、英単語のToDoアプリを作りながら`Room`周辺の使い方を学べます。全体的にDIパターンを採用していますが、今回は**Hiltを使用してDIを自動化**します。

それでは、参りましょう！！

# この記事の対象者
* DIの概念やHiltの使い方を多少理解している方
* (かつ)上記のCodelabを参照して実装したコードをHiltを使ってリファクタリングしたい方

# 開発環境
* MacBook Air
* Android Studio Electric Eel | 2022.1.1
* Kotlin
* compileSdkVersion 33
* minSdkVersion 21

# DIのイメージ図

![hilt___-1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2456250/f76447e5-3331-05ee-7d76-46a624908936.png)

# 実装方法

## 1.build.gradleファイルに追加
執筆時点でのHiltのバージョンは2.44ですが、適宜変更してください。

```build.gradle(project)
plugins {
    id 'com.google.dagger.hilt.android' version '2.44' apply false
}
```

```build.gradle(app)
plugins {
    id 'com.google.dagger.hilt.android'
}

dependencies {
    //Hilt
    implementation "com.google.dagger:hilt-android:2.44"
    kapt "com.google.dagger:hilt-compiler:2.44"
}

kapt {
    correctErrorTypes true
}
```
## 2.ApplicationクラスにHilt定義
**リファクタリング前**
```WordsApplication.kt
class WordsApplication : Application() {
    val database by lazy { WordRoomDatabase.getDatabase(this) }
    val repository by lazy { WordRepository(database.wordDao()) }
}
```

Applicationクラスで`RoomDatabase`と`Repository`のインスタンスを生成しています。これらは「6.(リファクタリング前)」で使用します。

**リファクタリング後**
```WordsApplication.kt
@HiltAndroidApp
class WordsApplication : Application() {}
```

Applicationクラスに`@HiltAndroidApp`を付けることで、**アプリでHiltを使用できるように設定**します。RoomDatabaseとRepositoryのインスタンスは後に生成・注入するので、中身は空っぽで良いです。

## 3.Hiltモジュール作成
**リファクタリング前**
```WordRoomDatabase.kt
@Database(entities = arrayOf(Word::class), version = 1, exportSchema = false)
public abstract class WordRoomDatabase : RoomDatabase() {

   abstract fun wordDao(): WordDao

   companion object {
        @Volatile
        private var INSTANCE: WordRoomDatabase? = null

        fun getDatabase(context: Context): WordRoomDatabase {
            return INSTANCE ?: synchronized(this) {
                val instance = Room.databaseBuilder(
                        context.applicationContext,
                        WordRoomDatabase::class.java,
                        "word_database"
                    ).build()
                INSTANCE = instance
                instance
            }
        }
   }
}
```
シングルトンとして定義したcompanion object内の`getDatabase()`で、`RoomDatabase`を作成・返します。

**リファクタリング後**
```WordRoomDatabase.kt
@Database(entities = arrayOf(Word::class), version = 1, exportSchema = false)
public abstract class WordRoomDatabase : RoomDatabase() {
   abstract fun wordDao(): WordDao
}
```
上記companion object内の中身は次の`RoomModule`で引き継ぐため、削除します。

```RoomModule.kt
@Module
@InstallIn(SingletonComponent::class)
object RoomModule {

    @Singleton
    @Provides
    fun provideDatabase(
        @ApplicationContext context: Context
    ) = Room.databaseBuilder(context, WordRoomDatabase::class.java, "word_database").build()

    @Singleton
    @Provides
    fun provideDao(db: WordRoomDatabase) = db.wordDao()
}
```

objectに`@Module`と`@InstallIn`を付けることで、Hiltモジュール作成します。**Hiltモジュールはコンストラクタ注入ができない型を含めて様々なインスタンスの提供方法をHiltに伝えます。**

`@Singleton`と`@Provides`を付けた関数により、関数のインスタンスをシングルトンとして提供します。今回は`RoomDatabase`と`DAO`のインスタンスの二つを提供します。

## 4.Repositoryに依存関係を注入
**リファクタリング前**
```WordRepository.kt
class WordRepository(private val wordDao: WordDao) {
   (省略)
}
```

**リファクタリング後**
```WordRepository.kt
class WordRepository @Inject constructor(private val wordDao: WordDao) {
    (省略)
}
```
「3.」のHiltモジュールから持ってきた**DAOインスタンスをRepositoryに注入**します。注入方法は、`@Inject`によるConstructor Injectionです。

## 5.ViewModelに依存関係を注入
**リファクタリング前**
```WordViewModel.kt
class WordViewModel(private val repository: WordRepository) : ViewModel() {
   (省略)
}

class WordViewModelFactory(private val repository: WordRepository) : ViewModelProvider.Factory {
    override fun <T : ViewModel> create(modelClass: Class<T>): T {
        (省略)
    }
}
```
独自の`Factory`を定義することで、ViewModel内でコンストラクタの引数を使用できます。

**リファクタリング後**
```WordViewModel.kt
@HiltViewModel
class WordViewModel　@Inject constructor(
    private val repository: WordRepository) : ViewModel() {
   (省略)
}
```
ViewModelに依存関係を注入する場合、`@HiltViewModel`を付けます。**ViewModelに「4.」のRepositoryをConstructor Injectionで注入**していきます。勿論Factoryの定義は必要ありません。

## 6.Activityに依存関係を注入
**リファクタリング前**
```MainActivity.kt
class MainActivity : AppCompatActivity() {

    private val wordViewModel: WordViewModel by viewModels {
        WordViewModelFactory((application as WordsApplication).repository)
    }
    
    override fun onCreate(savedInstanceState: Bundle?) {
        (省略)
    }
}
```
「2.(リファクタリング前)」で生成した`Repository`インスタンスを使って、ViewModelを作成します。

**リファクタリング後**
```MainActivity.kt
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {

    private val wordViewModel: WordViewModel by viewModels()
    
    override fun onCreate(savedInstanceState: Bundle?) {
        (省略)
    }
}
```
Activityに依存関係を注入する場合、`@AndroidEntryPoint`を付けます。**Activityに「5.」のViewModelを`by viewModels()`で注入**していきます。

# おわりに

今回は公式チュートリアルCodelabのRoom編で扱うコードを、`Hilt`を使ったプログラムへリファクタリングする方法を書きました。

Hilt導入前では、ApplicationクラスやFactoryなど手動で依存関係を設定する必要がありました。しかし導入後では、**アノテーションでHiltに指示をすることで自動で依存関係を把握し、注入**してくれます。楽ですよね。

以上です。ありがとうございました！

# おまけ
上記の方法をもとに、Android開発で習得・使用した技術を管理できる簡単なサンプルアプリをDI風に仕上げました。参考までに。

https://github.com/seiyaleb/AndroidTechManageApp

# 参考サイト
* [Android アプリでの Hilt の使用](https://developer.android.com/codelabs/android-hilt?hl=ja#0)
* [Hilt を使用した依存関係の注入](https://developer.android.com/training/dependency-injection/hilt-android?hl=ja)
* [【はじめてのHilt】Hilt + Room で依存関係注入してみた](https://qiita.com/k__yamamoto/items/5354d1f71b9d51c12f20)
* [#53 AndroidでToDoアプリを作る - Hiltの導入](https://blog.mokelab.com/53/android_todo9.html)
* [googlecodelabs/android-room-with-a-view](https://github.com/googlecodelabs/android-room-with-a-view)