はじめに
Androidアプリ開発において、Hilt は依存性注入(DI)を簡潔に実現するための公式ライブラリです。
本記事では、@HiltViewModel を導入する前後のコードを比較しながら、その導入メリットをわかりやすく解説します。
対象読者は、ViewModelをすでに使っており、DIの仕組みやHiltに興味のある方です。
環境
- Kotlin
- Jetpack Compose
- Hilt 2.x
- ViewModel (androidx.lifecycle)
- Clean Architecture(ViewModel / UseCase / Repository)
Before:Hilt導入前のコード
Hilt導入前は、ViewModelへの依存性の注入を ViewModelProvider.Factory を使って手動で行う必要がありました。
Repository
class UserRepository {
fun getUser(id: String): User {
// データ取得処理
return User(id, "山田 太郎")
}
}
UseCase
class GetUserUseCase(
private val userRepository: UserRepository
) {
operator fun invoke(id: String): User {
return userRepository.getUser(id)
}
}
ViewModel(Factory必須)
ViewModelにコンストラクタ引数がある場合、ViewModelProvider.Factory を ViewModel内にネストして手書きする必要があります。
ViewModelが増えるたびに、このFactoryも増えていきます。
class UserViewModel(
private val getUserUseCase: GetUserUseCase
) : ViewModel() {
private val _uiState = MutableStateFlow<User?>(null)
val uiState: StateFlow<User?> = _uiState.asStateFlow()
fun loadUser(id: String) {
_uiState.value = getUserUseCase(id)
}
// ❗ ViewModel自体にFactoryクラスをネストして手書きする必要がある
// ❗ コンストラクタ引数が増えるたびにFactoryの修正も必要になる
class Factory(
private val getUserUseCase: GetUserUseCase
) : ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
@Suppress("UNCHECKED_CAST")
return UserViewModel(getUserUseCase) as T
}
}
}
Activityでの利用
class UserActivity : AppCompatActivity() {
private lateinit var viewModel: UserViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// ❗ 依存を自前で生成・組み立てる必要がある
val repository = UserRepository()
val useCase = GetUserUseCase(repository)
val factory = UserViewModel.Factory(useCase)
viewModel = ViewModelProvider(this, factory)[UserViewModel::class.java]
}
}
😩 課題点
| 問題 | 内容 |
|---|---|
| ボイラープレートが多い | クラスごとに Factory を手書きする必要がある |
| 依存の組み立てが煩雑 | 呼び出し元で全依存を手動でインスタンス化する必要がある |
| テストしにくい | 依存の差し替えが困難でテストコードが複雑になる |
| スケールしない | クラスが増えるほど管理コストが増大する |
After:HiltViewModel導入後のコード
Hiltを導入することで、依存の生成・注入がすべて自動化されます。
セットアップ
build.gradle に以下を追加します。
// build.gradle.kts (project)
plugins {
id("com.google.dagger.hilt.android") version "2.51" apply false
}
// build.gradle.kts (app)
plugins {
id("com.google.dagger.hilt.android")
kotlin("kapt")
}
dependencies {
implementation("com.google.dagger:hilt-android:2.51")
kapt("com.google.dagger:hilt-android-compiler:2.51")
implementation("androidx.hilt:hilt-navigation-compose:1.2.0") // Compose利用時
}
Repository
// ✅ @Inject constructor を追加するだけ
@Singleton
class UserRepository @Inject constructor() {
fun getUser(id: String): User {
// データ取得処理
return User(id, "山田 太郎")
}
}
UseCase
// ✅ @Inject constructor を追加するだけ
class GetUserUseCase @Inject constructor(
private val userRepository: UserRepository
) {
operator fun invoke(id: String): User {
return userRepository.getUser(id)
}
}
ViewModel
@HiltViewModel と @Inject constructor を付与するだけで、ViewModel内のFactoryクラスが完全に不要になります。
Hiltがコンパイル時にFactoryを自動生成するため、引数が増えても手書きコードは一切不要です。
// ✅ @HiltViewModel + @Inject constructor を追加するだけ
// ✅ ViewModel内のFactoryクラスが完全に不要になる!
// ✅ コンストラクタ引数が増えてもFactoryの修正は不要
@HiltViewModel
class UserViewModel @Inject constructor(
private val getUserUseCase: GetUserUseCase
) : ViewModel() {
private val _uiState = MutableStateFlow<User?>(null)
val uiState: StateFlow<User?> = _uiState.asStateFlow()
fun loadUser(id: String) {
_uiState.value = getUserUseCase(id)
}
}
ActivityまたはComposableでの利用
// Activity
@AndroidEntryPoint
class UserActivity : AppCompatActivity() {
// ✅ by viewModels() で自動注入される
private val viewModel: UserViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
viewModel.loadUser("user_001")
}
}
// Jetpack Compose
@Composable
fun UserScreen(
// ✅ hiltViewModel() で自動注入される
viewModel: UserViewModel = hiltViewModel()
) {
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
// UI実装
}
Before / After 比較まとめ
| 項目 | Before(手動DI) | After(HiltViewModel) |
|---|---|---|
| Factoryクラス | 各ViewModelに手書き必要 | ✅ 不要 |
| 依存の組み立て | 呼び出し元で手動生成 | ✅ Hiltが自動で注入 |
| コード量 | 多い(ボイラープレート) | ✅ 大幅に削減 |
| テストのしやすさ | 差し替えが煩雑 | ✅ Hiltのテスト用APIで簡単 |
| スケーラビリティ | クラスが増えると管理困難 | ✅ 依存グラフをHiltが管理 |
HiltViewModel 導入の主なメリット
1. Factory クラスが不要になる
従来は ViewModelProvider.Factory を手動で実装する必要がありましたが、@HiltViewModel を付与するだけでHiltが自動的にFactoryを生成します。クラス数が増えても保守コストが増えません。
2. 依存の組み立てを Hilt に任せられる
@Inject constructor を各クラスに付与するだけで、Hiltが依存グラフを解析し、必要なインスタンスを自動で生成・注入します。呼び出し元で手動インスタンス化する必要がなくなります。
3. コードの見通しが大幅に改善される
ボイラープレートが排除されることで、ビジネスロジックに集中したコードを書けるようになります。ViewModelはシンプルに「何をするか」だけを記述すれば済みます。
4. テストが書きやすくなる
Hiltはテスト用のモジュール差し替え(@TestInstallIn)をサポートしており、依存の置き換えが宣言的に行えます。モックへの切り替えがシンプルになります。
@HiltAndroidTest
class UserViewModelTest {
@get:Rule
var hiltRule = HiltAndroidRule(this)
@Inject
lateinit var viewModel: UserViewModel
@Before
fun init() {
hiltRule.inject()
}
@Test
fun testLoadUser() {
viewModel.loadUser("test_id")
// アサーション
}
}
5. SavedStateHandle との連携も自動化
SavedStateHandle が必要なViewModelでも、追加設定なしでHiltが自動注入します。
@HiltViewModel
class UserDetailViewModel @Inject constructor(
savedStateHandle: SavedStateHandle, // ✅ 自動注入される
private val getUserUseCase: GetUserUseCase
) : ViewModel() {
val userId: String = savedStateHandle["userId"] ?: ""
}
応用編:@AssistedInject + creationCallback で動的パラメータを渡す
この手法が必要になる場面
@HiltViewModel はHiltが管理する依存を自動注入してくれますが、画面遷移時に動的に決まるパラメータ(例:詳細画面のIDや初期フィルター値など)は、そのままでは渡せません。
SavedStateHandle 経由でも渡せますが、型安全にコンストラクタで受け取りたい場合に @AssistedInject + creationCallback が有効です。
| 手法 | 向いているケース |
|---|---|
SavedStateHandle |
Navigation引数・プロセス再起動後の復元が必要 |
@AssistedInject |
型安全に動的パラメータをコンストラクタで受け取りたい |
セットアップ(追加不要)
@AssistedInject は Hilt(Dagger)に内包されているため、追加ライブラリは不要です。
Repository / UseCase(変更なし)
@Singleton
class UserRepository @Inject constructor() {
fun getUser(id: String): User {
return User(id, "山田 太郎")
}
}
class GetUserUseCase @Inject constructor(
private val userRepository: UserRepository
) {
operator fun invoke(id: String): User {
return userRepository.getUser(id)
}
}
ViewModel:@AssistedInject で動的パラメータを受け取る
// ✅ @HiltViewModel ではなく @HiltViewModel(assistedFactory = ...) を使う
@HiltViewModel(assistedFactory = UserDetailViewModel.Factory::class)
class UserDetailViewModel @AssistedInject constructor(
// Hiltが管理する依存 → 通常通り自動注入
private val getUserUseCase: GetUserUseCase,
// 動的パラメータ → @Assisted で受け取る
@Assisted val userId: String
) : ViewModel() {
private val _uiState = MutableStateFlow<User?>(null)
val uiState: StateFlow<User?> = _uiState.asStateFlow()
init {
loadUser()
}
private fun loadUser() {
_uiState.value = getUserUseCase(userId)
}
// ✅ @AssistedFactory でFactoryインターフェースを定義
@AssistedFactory
interface Factory {
fun create(userId: String): UserDetailViewModel
}
}
Composableでの利用:creationCallback で Factory を渡す
@Composable
fun UserDetailScreen(
userId: String,
// ✅ creationCallback で AssistedFactory を使ってViewModelを生成
viewModel: UserDetailViewModel = hiltViewModel<UserDetailViewModel, UserDetailViewModel.Factory>(
creationCallback = { factory -> factory.create(userId) }
)
) {
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
when (val user = uiState) {
null -> CircularProgressIndicator()
else -> Text(text = user.name)
}
}
Activityでの利用(Compose非使用の場合)
@AndroidEntryPoint
class UserDetailActivity : AppCompatActivity() {
private val userId: String by lazy {
intent.getStringExtra("USER_ID") ?: ""
}
private val viewModel: UserDetailViewModel by viewModels(
extrasProducer = {
// ✅ creationCallback で Factory を渡す
MutableCreationExtras(defaultViewModelCreationExtras).apply {
set(
ViewModelProvider.NewInstanceFactory.VIEW_MODEL_KEY,
UserDetailViewModel::class.java.name
)
}
}
) {
// Factory を使って ViewModel を生成
val factory = HiltViewModelFactory(this, intent.extras)
object : ViewModelProvider.Factory {
@Suppress("UNCHECKED_CAST")
override fun <T : ViewModel> create(
modelClass: Class<T>,
extras: CreationExtras
): T {
val assistedFactory = (factory as HasDefaultViewModelProviderFactory)
.defaultViewModelProviderFactory
return UserDetailViewModel(
getUserUseCase = TODO("Hiltが解決"),
userId = userId
) as T
}
}
}
}
💡 ポイント:Activity利用の場合はCompose +
hiltViewModel(creationCallback = ...)の方がシンプルです。Composeへの移行が難しい場合はSavedStateHandle経由との併用も検討してください。
@AssistedInject を使った全体のデータフロー
Composable
└─ hiltViewModel<VM, VM.Factory>(creationCallback = { factory -> factory.create(userId) })
│
├─ VM.Factory(@AssistedFactory) ← Hiltが自動生成
│ └─ factory.create(userId)
│
└─ UserDetailViewModel
├─ getUserUseCase ← Hiltが自動注入
└─ userId ← creationCallback経由で注入(@Assisted)
@AssistedInject 導入のメリット
| メリット | 内容 |
|---|---|
| 型安全 | 動的パラメータをコンストラクタで型安全に受け取れる |
| Hiltとの共存 | Hilt管理の依存と動的パラメータを1つのコンストラクタで混在できる |
| ボイラープレート削減 | 手動Factoryの大部分をHiltが自動生成してくれる |
| テスタブル |
@AssistedFactory のモックを差し替えるだけでテスト可能 |
まとめ
@HiltViewModel の導入により、以下が実現できます。
- Factory クラスの撤廃 → コード量の削減
- 依存の自動注入 → 呼び出し元のシンプル化
- テスタビリティの向上 → 依存の差し替えが容易に
- スケーラブルな設計 → クラスが増えても管理コストが増えない
-
動的パラメータも型安全に →
@AssistedInject+creationCallbackで画面遷移パラメータをコンストラクタで安全に受け取れる
Clean Architectureとの相性も非常に良く、ViewModel / UseCase / Repository それぞれへの @Inject constructor の追加という最小限の変更で、強力なDI基盤を手に入れることができます。
動的パラメータが必要な場面では @AssistedInject と creationCallback を組み合わせることで、型安全性を保ちながらHiltの恩恵をフルに享受できます。
既存プロジェクトへの段階的な導入も容易なため、ぜひ取り入れてみてください!