はじめに
本記事は Swift/Kotlin愛好会 Advent Calendar 2020 の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」にプラグインを追加します。
buildscript {
ext {
+ hilt_version = '2.31-alpha'
}
repositories {
google()
jcenter()
}
dependencies {
+ classpath "com.google.dagger:hilt-android-gradle-plugin:$hilt_version"
}
}
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"
}
以下はなくても動作しましたが、公式ドキュメントに記載されているので追加しています。
詳しい方がいたら教えていただけると嬉しいです
android {
+ compileOptions {
+ sourceCompatibility JavaVersion.VERSION_1_8
+ targetCompatibility JavaVersion.VERSION_1_8
+ }
}
+ kapt {
+ correctErrorTypes = true
+ }
アプリケーションクラスの実装
アプリケーションクラスに @HiltAndroidApp
アノテーションを付けます。
これにより、アプリケーションレベルのコンポーネントがHiltで使えるようになります。
要は「このアプリではHiltを使いますよー」というおまじないです。
+ import dagger.hilt.android.HiltAndroidApp
+ @HiltAndroidApp
class UhooiPicBookApp : Application() {
// ...
}
これでHiltのセットアップは完了です。
使い方
①インターフェースを通してコンストラクタインジェクションする
Before
例として、以下のインターフェースとその実装クラスを用意します。
interface MonstersRepository {
// ...
}
class MonstersFirestoreClient : MonstersRepository {
// ...
}
上記のインターフェースを通して、以下のビューモデルにDIする方法を紹介します。
現状ではビューモデル内で実装クラスのインスタンスを生成しているため、DIできていません。
class MonsterListViewModel : ViewModel() {
private val repository: MonstersRepository = MonstersFirestoreClient() // TODO: DIする
}
After
ビューモデルのコンストラクタに @Inject
アノテーションを付け、コンストラクタでインターフェースを型とした引数を用意します。
これにより、Hiltが自動的に対象インターフェースの実装クラスをDIします。
+ import javax.inject.Inject
- class MonsterListViewModel : ViewModel() {
+ class MonsterListViewModel @Inject constructor(
+ private val repository: MonstersRepository
+ ) : ViewModel() {
- private val repository: MonstersRepository = MonstersFirestoreClient() // TODO: DIする
}
実装クラスのコンストラクタにも @Inject
アノテーションを付ける必要があります。
+ import javax.inject.Inject
- class MonstersFirestoreClient : MonstersRepository {
+ class MonstersFirestoreClient @Inject constructor() : MonstersRepository {
// ...
}
しかし、インターフェースの実装クラスは複数あるかもしれないので、これだけではHiltがどの実装クラスをDIしていいかわかりません。
そこで用意するのがHiltモジュールです。
Hiltモジュール内で「このインターフェースにはこの実装クラスをDIする」ことを定義し、Hiltにインスタンスの提供方法を伝えます。
Hiltモジュールを新規作成します。
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
アノテーションを付けます。
関数内でインスタンスを生成して返します。
@Module
@InstallIn(SingletonComponent::class)
object AppModule {
@Probides
fun provideGoogleRepository(): GoogleRepository {
return GoogleDBClient()
}
③ビューモデルをフィールドインジェクションする
Before
例として、以下のフラグメントに先ほど紹介した MonsterListViewModel
をフィールドインジェクションする方法を紹介します。
class MonsterListFragment : Fragment() {
private val viewModel: MonsterListViewModel // TODO: DIする
}
import javax.inject.Inject
class MonsterListViewModel @Inject constructor(
private val repository: MonstersRepository
) : ViewModel() {
// ...
}
After
ビューモデルに @HiltViewModel
アノテーションを付けます。
+ import dagger.hilt.android.lifecycle.HiltViewModel
import javax.inject.Inject
+ @HiltViewModel
class MonsterListViewModel @Inject constructor(
private val repository: MonstersRepository
) : ViewModel() {
// ...
}
フラグメントに @AndroidEntryPoint
アノテーションを付け、ビューモデルの変数に by viewModels()
を付ければ、自動的にDIされます。
+ 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
とする)にもアノテーションを付ける必要があるということです。
+ import dagger.hilt.android.AndroidEntryPoint
+ @AndroidEntryPoint
class MainActivity : AppCompatActivity() {
// ...
}
viewModels()
は対象のクラス(今回は MonsterListFragment
)のみで生存するようです。
MonsterListFragment
を使用しているアクティビティ( MainActivity
)でも生存したい場合は activityViewModels()
とすればいいようです。
おまけ: チートシート
公式でHiltのチートシートが公開されています。
一通り目を通してから、公式ドキュメントでキャッチアップするのがいいかもしれません。
おわりに
少ないコード量でDIを実現できました!
Hiltにはできることがまだまだたくさんあるので、必要に応じて勉強していきたいです。
以上、 Swift/Kotlin愛好会 Advent Calendar 2020 の21日目の記事でした。