Hilt × Roomで理解する依存性注入アノテーション入門
📝 はじめに
この記事では、Androidで
- Hiltを使ってRoomデータベースを注入する典型的なパターン
を例にしながら、Hiltの各種アノテーション(デコレータ)の
- 何のためにあるのか(役割)
- どこに貼るものなのか(対象)
- いつ効いているのか(ライフサイクル)
を整理して解説します。
「とりあえずコピペで動いたけど、アノテーションの意味は全然わかってない」状態から、「自分で設計し直せる」くらいまで解像度を上げるのがゴールです。
🧭 全体像:Room + Hiltで何が行われているか
典型的な構成は次のようになります。
-
Application クラス
- @HiltAndroidApp を付ける
- Hiltがアプリ全体のDIコンテナ(グラフ)を自動生成する入口
-
RoomDatabase の定義
- abstract class AppDatabase : RoomDatabase()
- Dao を返す抽象メソッドを持つ
-
HiltのModuleで「提供する側」を定義
- @Module, @InstallIn(SingletonComponent::class)
- @Provides fun provideDatabase(...) : AppDatabase
- @Provides fun provideUserDao(db: AppDatabase) : UserDao
-
依存を「受け取る側」
- @HiltViewModel 付きの ViewModel
- constructor に @Inject を付けて UserDao や Repository を受け取る
- Activity / Fragment 側は @AndroidEntryPoint を付けて ViewModel を取得
ざっくり図にするとこうです。
Application(@HiltAndroidApp) ↓ HiltがDIグラフを作る @Module(@InstallIn(SingletonComponent)) → AppDatabase / Dao / Repository を「コンテナに登録」 @AndroidEntryPoint Activity / Fragment → @HiltViewModel ViewModel を解決 → コンストラクタ @Inject で Dao / Repository を解決
この流れを踏まえたうえで、アノテーション一つずつを見ていきます。
🧬 Hiltが生まれた背景(Dagger → Hilt)
Hiltは「Dagger 2 をAndroidでまともに使えるようにするためのラッパー + ベストプラクティス集」のような位置づけです。
-
Dagger 2
- 高性能・型安全なDIフレームワークだが、コンポーネント定義やスコープ設計が複雑
- Android固有のライフサイクル(Application / Activity / Fragment / ViewModel等)に1から設計を合わせるのが重い
-
Hilt
- 「Androidならこういうコンポーネント階層で使うよね」をあらかじめ用意
- ApplicationComponent(現在は SingletonComponent に改名)、ActivityComponent、ViewModelComponent 等が最初から揃っている
- @AndroidEntryPoint や @HiltViewModel など「Android向けに最適化されたアノテーション」が追加
Hiltを使うことで、DIそのものの抽象設計に時間を溶かさず、「どのスコープで何を使い回すか」だけに集中できるのが大きなメリットです。
🧱 Room + Hilt の最小サンプル(俯瞰用)
記法としてざっくりイメージだけ載せます(コンパイル用ではなく説明用)。
- Application クラス
@HiltAndroidApp class MyApplication : Application()
- RoomDatabase
@Database( entities = [UserEntity::class], version = 1, exportSchema = false ) abstract class AppDatabase : RoomDatabase() { abstract fun userDao(): UserDao }
- Hilt Module(DBとDaoを提供)
@Module @InstallIn(SingletonComponent::class) object DatabaseModule {
@Provides
@Singleton
fun provideDatabase(
@ApplicationContext context: Context
): AppDatabase =
Room.databaseBuilder(
context,
AppDatabase::class.java,
"app-db"
).build()
@Provides
fun provideUserDao(
db: AppDatabase
): UserDao = db.userDao()
}
- ViewModel(依存をコンストラクタで受け取る)
@HiltViewModel class UserListViewModel @Inject constructor( private val userDao: UserDao ) : ViewModel() { // ここで userDao を使う }
- Activity(ViewModelを注入)
@AndroidEntryPoint class MainActivity : ComponentActivity() { private val viewModel: UserListViewModel by viewModels() }
これをベースに、アノテーションを分解していきます。
🏁 アプリ全体をHilt対応にするアノテーション
🧷 @HiltAndroidApp
貼る場所
- Applicationクラス
役割
-
Hiltに「ここからアプリ全体のDIグラフを作ってくれ」と指示するトリガー
-
このアノテーションを付けると、Hiltが内部で
- Application用のコンポーネント(SingletonComponentのルート)を生成
- そこに @InstallIn(SingletonComponent::class) のModuleをぶら下げる
ポイント
- これがないと、そもそもHiltのDIコンテナ自体が立ち上がらない
- 1アプリに1つだけ
@HiltAndroidApp を付け忘れると、@AndroidEntryPoint を付けたActivity/Fragmentで「Hiltを初期化していない」系のエラーになります。まずここが起点だと覚えておくと迷いにくいです。
🎯 依存を「受け取る側」のアノテーション
🧷 @AndroidEntryPoint
貼る場所
- Activity, Fragment, Service, BroadcastReceiver 等(「Androidフレームワークに直接管理されるクラス」)
役割
- 「ここにDIで依存を入れたいので、このクラス用のコンポーネントを作ってくれ」とHiltに伝える
- 例えば Activity に付けると、Hiltは ActivityComponent を生成して、そこから ViewModel や他の依存を解決できるようにする
ポイント
- @AndroidEntryPoint を付けたクラスの中では、@Inject コンストラクタ付きのフィールドや ViewModel を安全に使える
- Fragment で使う場合は、その親の Activity も @AndroidEntryPoint が必要(Hiltのルール)
🧷 @HiltViewModel
貼る場所
- ViewModelクラス
役割
- 「このViewModelは Hilt 管理下の ViewModelComponent で管理する」と宣言する
- Hiltが ViewModel 用のコンポーネントを用意し、constructor @Inject で依存解決できるようにする
書く形 @HiltViewModel class UserListViewModel @Inject constructor( private val userDao: UserDao ) : ViewModel()
ポイント
- @HiltViewModel と constructor の @Inject はセットで考えると覚えやすい
- viewModels() デリゲートから自動的に Hilt 管理のViewModelが取れるようになる
🧷 @Inject(受け取る側)
貼る場所
-
主にコンストラクタ
- constructor @Inject constructor(...) の形
役割
- 「このクラスのインスタンスを作るときは、このコンストラクタを使え」とHiltに教える
- 引数に並んでいる型(UserDaoなど)を、HiltがDIグラフから探して注入する
ポイント
- クラスを「依存を受け取る側」としてDIグラフに参加させる宣言
- Hiltにとっては「インスタンス生成方法のレシピ」
🧰 依存を「提供する側」のアノテーション
🧷 @Module
貼る場所
-
object または class(通常は object にすることが多い)
- object DatabaseModule { ... }
役割
- 「この中のメソッドで、依存(インスタンス)を提供するよ」と宣言する箱
- Module自体は「提供方法の集まり」であり、それ自体がインスタンスになるわけではない
🧷 @InstallIn(...)
貼る場所
- @Module とセットで、Module クラスの上
例 @Module @InstallIn(SingletonComponent::class) object DatabaseModule
役割
-
「このModuleが提供する依存は、このコンポーネントのライフサイクルで管理してくれ」とHiltに伝える
-
SingletonComponent::class を指定すると
- アプリ全体のライフサイクル(Applicationと同じ)で生き続ける依存になる
- DBのような「アプリ中で1個だけ」のものに適している
他の代表例
- ActivityRetainedComponent::class
- ViewModelComponent::class
- ActivityComponent::class など
「どんな種類の依存か?」ではなく、「どのライフサイクルで動いてほしいか?」でコンポーネントを選ぶ、という意識で選ぶと整理しやすくなります。
🧷 @Provides
貼る場所
- @Module の中のメソッド
例 @Provides @Singleton fun provideDatabase( @ApplicationContext context: Context ): AppDatabase = Room.databaseBuilder( context, AppDatabase::class.java, "app-db" ).build()
役割
- 「この戻り値の型(ここでは AppDatabase)のインスタンスを、こうやって作ってコンテナに登録してくれ」とHiltに教える
- メソッド引数にもDIされた依存を受け取れる → 例では、Context を @ApplicationContext 付きで受け取っている
ポイント
- 「new (あるいは builder) する処理」を @Provides に集約するイメージ
- コンストラクタに @Inject が付けられない(サードパーティクラス、ビルダー必要なクラス等)をDIグラフに参加させるときに使う
🧷 @Binds(今回は補足)
Room + DB ではあまり出てきませんが、インターフェイスと実装を結びつけるときに使います。
例 @Binds abstract fun bindUserRepository( impl: UserRepositoryImpl ): UserRepository
役割
- 「UserRepositoryが欲しいと言われたら、UserRepositoryImplのインスタンスを渡せ」とHiltに教える
- 実体生成は constructor @Inject 任せ、@Bindsは「どの実装を使うか」だけ指定するパターン
🧱 スコープとコンポーネント関連のアノテーション
🧷 @Singleton
貼る場所
- @Provides付きメソッド
- あるいは @Inject コンストラクタを持つクラス
役割
-
指定したコンポーネントのライフサイクル内で「1インスタンスだけ」にするスコープを意味する
-
@InstallIn(SingletonComponent::class) のModule内で @Singleton を付けると、
- アプリ起動~終了まで、同じインスタンスが使い回される
Room DBでの意味
- AppDatabase を @Singleton にする → アプリ内で1つのDBインスタンスだけを使い回す
- Daoは @Singleton でも @Singleton なしでも大きな問題にはなりにくい(Roomが内部でキャッシュしている)が、統一のため付けておく場合もある
RoomDatabaseをSingletonにしない(Activityごとに別インスタンスなど)と、ファイルアクセス競合やパフォーマンス悪化の原因になります。基本的に「AppDatabaseはSingletonComponent + @Singleton」と覚えておくのが安全です。
🧷 SingletonComponent(とその他のコンポーネント)
-
SingletonComponent
- Application全体に1つ
- @HiltAndroidApp から生えるDIグラフのルート
- DB、Repository、設定系クラスなど「アプリを通して共有するもの」に向く
-
ViewModelComponent
- 各ViewModelごとに1つ
- ViewModelごとに状態を持ちたいときに使う
-
ActivityRetainedComponent / ActivityComponent
- Activityのライフサイクルと連動する依存に使う
「どこにInstallInするか」は、
- どこまで共有してよいか
- どのタイミングで破棄されてほしいか
で決めます。
🌐 コンテキスト系アノテーション
🧷 @ApplicationContext
貼る場所
- @Providesメソッドの引数
- constructor @Inject の引数 など
役割
- 「これは Application の Context が欲しい」ということをHiltに伝えるためのQualifier(識別子)
例 @Provides @Singleton fun provideDatabase( @ApplicationContext context: Context ): AppDatabase = ...
ポイント
- ActivityContext が欲しいときは @ActivityContext を使う
- RoomDatabase はApplicationContextで作るのが基本 → Activityのライフサイクルに引きずられないようにするため
🧿 Qualifier系(@Named など)
Roomを複数作るケースや、同じ型だが意味の違う依存を扱うときに出てきます。
例:DBを2つ持つ場合
@Qualifier @Retention(AnnotationRetention.BINARY) annotation class MainDb
@Qualifier @Retention(AnnotationRetention.BINARY) annotation class LogDb
@Module @InstallIn(SingletonComponent::class) object DatabaseModule {
@Provides
@Singleton
@MainDb
fun provideMainDatabase(...): AppDatabase = ...
@Provides
@Singleton
@LogDb
fun provideLogDatabase(...): AppDatabase = ...
}
ViewModel側
class SomeViewModel @Inject constructor( @MainDb private val mainDb: AppDatabase ) : ViewModel() { ... }
Qualifierは「同じ型が複数あるときのラベル」。型だけでは区別できない場合に使います。
🧹 よく迷うポイントを整理する
1. Applicationに書くもの vs Moduleに書くもの
-
Application
- @HiltAndroidApp を付けるだけ
- 「Hiltを起動する」役目
-
Module
- 実際に AppDatabase / Dao / Repository を「どう作るか」を書く場所
- @Module + @InstallIn + @Provides / @Binds
ApplicationにRoom.databaseBuilder(...)を書かないのは、
- テストしにくくなる
- 設定の差し替えが難しい
- DIコンテナ外の「手書き配線」が増える
からです。
2. @AndroidEntryPoint と @HiltViewModel の関係
-
@AndroidEntryPoint
- Activity / Fragment が「DIグラフにぶら下がる」宣言
- そこから ViewModel や他の依存が引き出せるようになる
-
@HiltViewModel
- ViewModel が「ViewModelComponent にぶら下がる」宣言
- constructor @Inject で依存を受け取れる
Activity/Fragment → ViewModel の順に「DIグラフにつなぐ」という意識で見ると整理しやすいです。
3. @Inject コンストラクタ vs @Provides
-
@Inject コンストラクタ
- 自分で定義したクラス(Repositoryなど)なら、constructor @Inject と書くだけでDIグラフに参加できる
- 単純なクラスに向いている
-
@Provides
- サードパーティクラス、ビルダーが必要なクラス、RoomDatabaseのように builder経由でしか作れないクラスに使う
- 「作り方が特殊なもの」は @Provides に集約
RoomDatabaseは builder 必須なので @Provides 一択、という整理です。
「自前のクラスは constructor @Inject、ライブラリのクラスやbuilder必須のものは @Provides」というルールにすると、迷う場面がかなり減ります。
🚀 まとめと次のステップ
この記事では、RoomのデータベースをHiltで注入するケースを軸に、
-
@HiltAndroidApp / @AndroidEntryPoint / @HiltViewModel / @Inject → 依存を「受け取る側」
-
@Module / @InstallIn / @Provides / @Binds / @Singleton / @ApplicationContext → 依存を「提供・スコープ管理する側」
-
SingletonComponent などのコンポーネント → 「どのライフサイクルで生きるか」
といった役割を整理しました。
次にやると理解が一気に定着するのは、
- RoomDatabase の Module を自分で 0 から書き直してみる
- 「DBをActivityスコープにしたらどうなるか?」など、わざと変なスコープにして挙動を観察する
- Repository や UseCase もすべて constructor @Inject でつなぎ、手書きnewを排除してみる
あたりです。
もし、FreezerManの既存コードをベースに、
- 「このクラスにはどのコンポーネントを選ぶのが筋か?」
- 「この @Provides は Repositoryにリファクタできるか?」
といった形で具体的にレビューしたい場合は、その前提で個別ファイルを指定してくれれば、Hiltの設計チェック用の記事も書けます。