0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【Android】HiltViewModelで依存性注入を快適にする

0
Posted at

はじめに

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.FactoryViewModel内にネストして手書きする必要があります。
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基盤を手に入れることができます。

動的パラメータが必要な場面では @AssistedInjectcreationCallback を組み合わせることで、型安全性を保ちながらHiltの恩恵をフルに享受できます。

既存プロジェクトへの段階的な導入も容易なため、ぜひ取り入れてみてください!

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?