【Kotlin/Android】Hiltの導入方法と使い方!依存性注入(DI)とは?
この記事からわかること
- Android Studio/KotlinでHiltの使い方
- DI(依存性注入)とは?
- 導入方法と仕組み
index
[open]
\ アプリをリリースしました /
環境
- Android Studio:Meerkat
- Kotlin:2.1.10
- Material3
- Hilt:2.56.2
- AGP:8.9.2
- Gradle:8.11.1
- Mac M1:Sequoia 15.4
Hiltとは?
「Hilt」とはAndroidアプリ開発においてオブジェクト同士の依存関係を解決し、プロジェクト内のオブジェクトに対して依存性注入(DI)デザインパターンを活用するためのライブラリです。Googleが管理するライブラリで内部的にはDaggerというライブラリを使っており、学習コストが高いDaggerをラップして使いやすくしてあるのがHiltになります。
依存関係とは?
そもそも依存関係とはオブジェクト同士が依存している関係のことです。ここでいうオブジェクトはクラスや構造体、メソッドなども当てはまります。例えば以下はUserクラスがActionGameクラスに依存している関係になります。
class User {
var stamina: Int = 20
fun playGame() {
val actionGame = ActionGame()
if (actionGame.requiredStamina <= stamina) {
println("Playing")
stamina -= actionGame.requiredStamina
} else {
println("Out of Stamina...")
}
}
}
class ActionGame {
var requiredStamina: Int = 10
}
fun main() {
val user = User()
user.playGame()
println(user.stamina)
}
オブジェクト同士の依存関係やDIについての詳細は以下の記事を参考にしてください。
導入方法と前準備
公式リファレンス:Dependency injection with Hilt
Hiltを導入するためにはプロジェクトの「build.gradle」にkspとhiltの依存関係を追加します。
plugins {
// 〜〜〜〜〜〜
id 'org.jetbrains.kotlin.android' version '2.0.21' apply false
id 'com.google.dagger.hilt.android' version '2.56.2' apply false
}
続いてモジュール側の「build.gradle」にもkspとhiltの依存関係を追加します。
plugins {
id 'com.google.devtools.ksp'
id 'com.google.dagger.hilt.android'
}
// 〜〜〜〜〜〜
dependencies {
implementation "com.google.dagger:hilt-android:2.56.2"
ksp "com.google.dagger:hilt-compiler:2.56.2"
}
これで依存関係の設定は完了です。次にプロジェクト内でHiltを利用できるようにしていきます。
Hiltを利用できるようにするために@HiltAndroidAppアノテーションが付与されたApplicationクラスを実装する必要があります。Applicationを継承したクラスを実装済みであれば@HiltAndroidAppアノテーションを付与し、なければ新規で継承したクラスを空でも良いので定義しておいてください。
@HiltAndroidApp
class App : Application()
Applicationサブクラスを定義したら忘れずに「AndroidManifest.xml」ファイルに追加しておいてください。
<application
android:name=".App">
// 〜〜〜〜〜〜〜〜〜〜
</application>
Hiltを使用した実装例
Androidで依存関係インジェクションを行う主な方法は2つあります。
- コンストラクタインジェクション:ViewModel、Repository...
- フィールドインジェクション(セッターインジェクション):Activity、Fragment...
コンストラクタインジェクション
コンストラクタインジェクションはその名前の通りコンストラクタを使用して依存性を挿入する方法です。ViewModelやRepositoryなどシンプルな独自のクラスなどでは簡単にコンストラクタインジェクションを実装することができます。
フィールドインジェクション(セッターインジェクション)
ActivityやFragmentなどシステムによってインスタンス化されるクラスはコンストラクタインジェクションが不可能です。そのためクラスの作成後に依存関係がインスタンス化されます。
公式リファレンス:Android での依存関係インジェクション
コンストラクタインジェクション
例えばViewModelやRepositoryなど依存性注入(DI)を行うためには「コンストラクタインジェクション」という方法を用います。最初にRepositoryクラスのconstructorに対して@Injectアノテーションを付与します。引数でContextを受け取りたい場合などは@ApplicationContextを付与してあげてください。
class RootRepository @Inject constructor(
@ApplicationContext private val context: Context,
) {
// 実装〜〜〜〜〜〜
}
続いてViewModel側に@HiltViewModelアノテーションを付与し、こちらもconstructorに対して@Injectアノテーションを付与します。またViewModelはandroidx.lifecycle.ViewModelを継承させておきます。
おすすめ記事:【Kotlin/Android Studio】ViewModelの使い方!画面再構築のデータ保持
@HiltViewModel
class MainViewModel @Inject constructor(
private val rootRepository: RootRepository
): ViewModel() {
// 実装〜〜〜〜〜〜
}
最後にViewModelの呼び出し側(Activity)に@AndroidEntryPointを付与します。ViewModelのインスタンス化はviewModels()を使用します。
@AndroidEntryPoint
class MainActivity: AppCompatActivity() {
private val viewModel: MainViewModel by viewModels()
// 実装〜〜〜〜〜〜
}
これでコンストラクタインジェクションの実装は完了です。
フィールドインジェクション
コンストラクタを経由できないActivityやFragmentに依存性注入(DI)を行うためには「フィールドインジェクション」という方法を使用します。Repositoryの記載は先ほどと変わらずActivity側で@Injectアノテーションを付与するのがlateinitで定義した変数になります。
おすすめ記事:【Kotlin/Android】lateinit(遅延初期化プロパティ)とは?使い方
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
@Inject
lateinit var rootRepository: RootRepository
}
lateinitを使用しているのでsuper.onCreateが実行された後でないと参照できないので注意してください。
@ActivityContextと@ApplicationContext
Contextを注入するためのアノテーションとして@ActivityContextと@ApplicationContextがあります。両者の違いは「アプリ全体に依存したContext」か「Activityに依存したContext」かという違いです。
| アノテーション | 概要 | 使用用途 |
|---|---|---|
| @ActivityContext | Activityに依存したContext | DB操作, DIのModule, Repositoryなど |
| @ApplicationContext | アプリ全体に依存したContext | Dialog, Toast, View関連のUI操作など |
UIに絡む部分には@ActivityContextを使ってあげる必要がありますが、適切に解放しないとメモリリークを引き起こすので注意してください。
インターフェースで抽象度をあげる:@Module / @Provides
先ほどのRootRepositoryを共通のインターフェースを定義して継承させ、モックなどに切り替えやすくテストしやすい設計にしたいと思います。その場合先ほどのコンストラクタインジェクションのままだと期待通りに実装できないので@Moduleと@Providesアノテーションを使用していきます。
まずはRootRepositoryのインターフェースを定義します。RootRepositoryと言う名前はインターフェースで使用したいので元のクラスはRootRepositoryImplへ変更します。
/** アプリ内で扱うPersonデータのRepositoryインターフェース */
interface RootRepository {
fun fetchAllUser(callback: (List<Person>) -> Unit)
}
RootRepositoryImplはRootRepositoryを継承させるくらいで特に変化はありません。
/** アプリ内で扱うPersonデータのRepository実態 */
class RootRepositoryImpl @Inject constructor(
@ApplicationContext private val context: Context,
): RootRepository {
override fun fetchAllUser(callback: (List<Person>) -> Unit) {
// 〜〜〜〜〜〜
}
}
肝となるのがRepositoryModuleクラスです。このモジュールクラスが実際に注入する依存関係を定義するクラスになっています。@Moduleアノテーションを付与して依存性提供の定義をする場所であることを示し、@Providesで対象のインターフェースにどの実態を渡すかを定義します。
/**
* Hilt に対して Repository 関連の依存関係をどのように提供するかを定義するクラス
*
* このモジュールはアプリケーション全体(Singletonスコープ)で有効
* RootRepository(インターフェース)を要求されたときに、RootRepositoryImpl(実装)を注入することを定義
*/
@Module // Hiltに依存性提供の定義をする場所であることを示す
@InstallIn(SingletonComponent::class) // アプリ全体で共有する依存性(シングルトン)であることを示す
object RepositoryModule {
@Provides // 対象のインターフェースにどの実態を渡すかを定義する
fun provideRootRepository(
impl: RootRepositoryImpl
): RootRepository = impl
}
ViewModel側の変更は特にありません。RootRepository型を引数で受け取ることを明示的に記載しているだけですが、Hiltが実行時にRootRepositoryImplが連携づけられていることをRepositoryModule確認し、自動で注入してくれます。
@HiltViewModel
class MainViewModel @Inject constructor(
private val rootRepository: RootRepository
): ViewModel() {
// 実装〜〜〜〜〜〜
}
Jetpack Composeで使用する
「Jetpack Compose」でHiltを使用する方法が少し異なったのでまとめておきます。Jetpack Compose自体は以下の記事を参考にしてください。
Jetpack ComposeでHiltを使用するためには以下の依存関係を追加しておきます。
// Hilt for Navigation Compose
implementation "androidx.hilt:hilt-navigation-compose:1.2.0"
あとは基本的に変わりはないですがViewModelを入れ込む際にhiltViewModelメソッドが使えるようになります。hiltViewModelメソッドではComposable関数の引数でデータ型を指定するだけで最適なViewModelを導入することができます。
@Composable
fun MyAppScreen(
viewModel: MyViewModel = hiltViewModel()
) {
val text by viewModel.text.collectAsState()
Text(text)
}
hiltViewModelメソッドを使用することでNavBackStackEntryのライフサイクルに従ってViewModelを破棄してくれるようになります。
まだまだ勉強中ですので間違っている点や至らぬ点がありましたら教えていただけると助かります。
ご覧いただきありがとうございました。






