「またViewModelFactory
を作るのか…」「画面が増えるたびに、このお決まりのコードを書くのが面倒だな…」
Jetpack Composeで開発をしていると、そんな風に感じたことはありませんか?
**依存性注入(DI)**は、モダンなアプリ開発に不可欠な設計パターンですが、手動での実装は定型コード(ボイラープレート)の温床になりがちです。
この記事では、その面倒な手作業をGoogle推奨のDIライブラリであるHiltに任せ、DIを完全に自動化するための詳細な設計と手順を解説します。このリファクタリングを終えれば、あなたのアプリの基盤はより堅牢になり、機能追加のスピードは格段に向上するでしょう。他の開発者が見ても再現できるように、「なぜそうするのか」という思考の背景と、具体的なコードをステップ・バイ・ステップで詳細に解説します。
1. なぜHiltなのか? (Before & After)
まず、なぜこのリファクタリングを行うのか、目的を明確にしましょう。
Before: 手動DI(ViewModelFactory
)の問題点
現在の私たちのアプリは、ViewModelFactory
という「部品の組み立て説明書」を画面ごとに手作りしています。
-
問題点:
- 画面や
ViewModel
が増えるたびに、新しいFactory
を作る手間がかかる。 -
SavedStateHandle
のように、ViewModel
が必要とする部品が増えると、Factory
の実装が複雑になる。 - 単体テストの際に、部品の差し替え(モック化)が少し面倒。
- 画面や
After: Hiltによる自動DIのメリット
Hiltは、いくつかの**アノテーション(@
から始まる魔法の印)**を付けるだけで、DIに関する面倒な作業をすべて裏側で自動的に行ってくれるライブラリです。
-
メリット:
-
ViewModelFactory
が完全に不要になる。 -
@HiltViewModel
と付けるだけで、ViewModel
の生成とライフサイクル管理を自動化。 -
@Inject
と付けるだけで、必要な部品(例:Repository
)を自動で注入してくれる。 - コードが劇的にシンプルになり、開発者はビジネスロジックの実装に集中できる。
-
今日の目標は、この**「手作業からの解放」**を実現することです!
2. Hilt導入の詳細設計(ステップ・バイ・ステップ)
それでは、具体的な実装手順を詳細に見ていきましょう。
Step 1: Hiltプラグインとライブラリの導入
目的: Hiltを動作させるために必要な万能工具を、プロジェクトのGradleファイルにセットアップする。
作業内容:
-
build.gradle.kts
(プロジェクトレベル) を開きます。-
plugins
ブロックにHiltのプラグインを追加します。これはプロジェクト全体のおまじないです。
-
// build.gradle.kts (Top-level)
plugins {
// ...
id("com.google.dagger.hilt.android") version "2.51.1" apply false // ★追加
}
-
app/build.gradle.kts
(モジュールレベル) を開きます。-
plugins
ブロックに、hilt-android
とksp
の2つのプラグインを追加します。 -
dependencies
ブロックに、Hilt本体と、コードを自動生成してくれるcompiler
の2つのライブラリを追加します。
-
// app/build.gradle.kts
plugins {
// ...
id("com.google.dagger.hilt.android") // ★追加
id("com.google.devtools.ksp") // ★追加
}
dependencies {
// ...
implementation("com.google.dagger:hilt-android:2.51.1") // ★追加
ksp("com.google.dagger:hilt-compiler:2.51.1") // ★追加
// ViewModelをHiltで扱うために必要
implementation("androidx.hilt:hilt-navigation-compose:1.2.0") // ★追加
}
- Android Studioの上部に表示される「Sync Now」をクリックし、プロジェクトに変更を反映させます。
Step 2: Applicationクラスへのアノテーション付与
目的: Hiltに「このアプリは僕がDIを管理するからね!」と教えるための、アプリの起動スイッチを入れる。
作業内容:
MemoApplication.kt
を開き、クラス宣言の上に@HiltAndroidApp
というアノテーションを一行追加します。
// MemoApplication.kt
package com.example.simplememoapp_android
import android.app.Application
import dagger.hilt.android.HiltAndroidApp // ★インポート
@HiltAndroidApp // ★この一行を追加!
class MemoApplication : Application() {
// このクラスの中身は後で空になる
}
Step 3: DIモジュールの作成
目的: Repository
やDatabase
といった部品の**作り方(レシピ)**をHiltに教えるための「設計図(モジュール)」を作成する。
作業内容:
-
com.example.simplememoapp_android
パッケージの下にdi
という新しいパッケージを作成します。 -
di
パッケージの中にAppModule.kt
という新しいファイルを作成し、以下のコードを記述します。
// di/AppModule.kt (新規作成)
package com.example.simplememoapp_android.di
import android.app.Application
import androidx.room.Room
import com.example.simplememoapp_android.data.local.AppDatabase
import com.example.simplememoapp_android.data.local.dao.MemoDao
import com.example.simplememoapp_android.data.repository.MemoRepository
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class) // アプリの生存期間中、インスタンスが共有されることを示す
object AppModule {
@Provides
@Singleton // このレシピで作られるインスタンスはアプリ内で常に1つだけ(シングルトン)
fun provideAppDatabase(app: Application): AppDatabase {
return Room.databaseBuilder(
app,
AppDatabase::class.java,
"memo_database"
)
.addMigrations(AppDatabase.MIGRATION_1_2)
.build()
}
@Provides
@Singleton
fun provideMemoDao(db: AppDatabase): MemoDao {
return db.memoDao()
}
@Provides
@Singleton
fun provideMemoRepository(dao: MemoDao): MemoRepository {
return MemoRepository(dao)
}
}
【解説】
-
@Module
: 「これは部品の作り方を教える設計図ですよ」という印。 -
@InstallIn
: この設計図が、どの範囲で有効か(今回はアプリ全体)を指定。 -
@Provides
: 「この関数が、部品の具体的な作り方(レシピ)ですよ」という印。 -
@Singleton
: このレシピで作った部品は、アプリ内で使い回す(一つしか作らない)ことを指定。
Step 4: ViewModelのリファクタリング
目的: Hiltの魔法@HiltViewModel
を使い、手作りしてきたViewModelFactory
をすべて削除する。
作業内容:
-
MemoListViewModel.kt
とMemoDetailViewModel.kt
を開き、アノテーションを修正します。 -
ui/viewmodel
パッケージから、MemoListViewModelFactory.kt
とMemoDetailViewModelFactory.kt
の2つのファイルを右クリックしてDelete
します。 さようなら、手作業!
// ui/viewmodel/MemoListViewModel.kt (修正)
import androidx.lifecycle.ViewModel
import com.example.simplememoapp_android.data.repository.MemoRepository
import dagger.hilt.android.lifecycle.HiltViewModel // ★インポート
import javax.inject.Inject // ★インポート
@HiltViewModel // ★アノテーションを変更
class MemoListViewModel @Inject constructor( // ★@Inject constructorを追加
private val repository: MemoRepository
) : ViewModel() {
// ... ViewModelの中身は一切変更なし! ...
}
MemoDetailViewModel
も同様に@HiltViewModel
と@Inject constructor
を付けて修正します。SavedStateHandle
はHiltが自動で用意してくれるので、Factory
は不要です。
Step 5: UI層の修正と最終クリーンアップ
目的: UI(Composable)が、Hilt経由でViewModel
を呼び出すように変更し、不要になった古いコードを掃除して完了。
作業内容:
-
MemoListScreen.kt
とMemoDetailScreen.kt
を開き、viewModel()
の呼び出しをhiltViewModel()
に修正します。
// ui/screen/MemoListScreen.kt (修正の一部)
import androidx.hilt.navigation.compose.hiltViewModel // ★インポートを変更
import androidx.compose.runtime.Composable
import androidx.navigation.NavController
import com.example.simplememoapp_android.ui.viewmodel.MemoListViewModel
@Composable
fun MemoListScreen(navController: NavController) {
// ▼▼▼ この部分はもう不要なので、完全に削除する! ▼▼▼
// val application = LocalContext.current.applicationContext as MemoApplication
// val repository = application.repository
// val viewModel: MemoListViewModel = viewModel(
// factory = MemoListViewModelFactory(repository)
// )
// ▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲
// ★★★ ViewModelの取得が、この一行だけで完了! ★★★
val viewModel: MemoListViewModel = hiltViewModel()
// ... 以下は変更なし ...
}
MemoDetailScreen.kt
も同様に、hiltViewModel()
を使ってMemoDetailViewModel
を取得するように修正します。
-
MemoApplication.kt
を再度開き、手動でRepository
を生成していたコードを完全に削除します。
MemoApplication.kt
(最終版)
package com.example.simplememoapp_android
import android.app.Application
import dagger.hilt.android.HiltAndroidApp
@HiltAndroidApp
class MemoApplication : Application() {
// 中身は空っぽでOK!
// Hiltがすべてを裏側で管理してくれます。
}
まとめ
以上で、Hiltの導入リファクタリングは完了です!
アプリを実行して、以前と全く同じように動作することを確認してみてください。見た目は変わりませんが、内部の構造は格段にモダンで、拡張しやすくなりました。
手作りのViewModelFactory
から卒業した今、あなたはより本質的な機能開発に集中できます。
次のステップ、ネットワーク通信機能の実装が、もう目の前ですね!