メインコンテンツへスキップ

Hilt × Roomで理解する依存性注入アノテーション入門

📝 はじめに

この記事では、Androidで

  • Hiltを使ってRoomデータベースを注入する典型的なパターン

を例にしながら、Hiltの各種アノテーション(デコレータ)の

  • 何のためにあるのか(役割)
  • どこに貼るものなのか(対象)
  • いつ効いているのか(ライフサイクル)

を整理して解説します。

「とりあえずコピペで動いたけど、アノテーションの意味は全然わかってない」状態から、「自分で設計し直せる」くらいまで解像度を上げるのがゴールです。

🧭 全体像:Room + Hiltで何が行われているか

典型的な構成は次のようになります。

  1. Application クラス

    • @HiltAndroidApp を付ける
    • Hiltがアプリ全体のDIコンテナ(グラフ)を自動生成する入口
  2. RoomDatabase の定義

    • abstract class AppDatabase : RoomDatabase()
    • Dao を返す抽象メソッドを持つ
  3. HiltのModuleで「提供する側」を定義

    • @Module, @InstallIn(SingletonComponent::class)
    • @Provides fun provideDatabase(...) : AppDatabase
    • @Provides fun provideUserDao(db: AppDatabase) : UserDao
  4. 依存を「受け取る側」

    • @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 の最小サンプル(俯瞰用)

記法としてざっくりイメージだけ載せます(コンパイル用ではなく説明用)。

  1. Application クラス

@HiltAndroidApp class MyApplication : Application()

  1. RoomDatabase

@Database( entities = [UserEntity::class], version = 1, exportSchema = false ) abstract class AppDatabase : RoomDatabase() { abstract fun userDao(): UserDao }

  1. 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()

}

  1. ViewModel(依存をコンストラクタで受け取る)

@HiltViewModel class UserListViewModel @Inject constructor( private val userDao: UserDao ) : ViewModel() { // ここで userDao を使う }

  1. 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を排除してみる

あたりです。