はじめに
こんにちは!Android開発者のみなさん、依存性注入(DI)で苦労していませんか?
私もAndroid開発を始めた頃は、DIなしで開発していました。でも、プロジェクトが大きくなるにつれて、クラス間の依存関係がスパゲッティコードになって、テストも書きにくくて…本当に大変でした😅
特にDaggerを使い始めた時は、あの複雑な設定に頭を悩ませていました。Component、Module、Scope…概念は理解できても、実際に書くとエラーばかり。
そんな悩みを一気に解決してくれるのが Hilt です!
Hiltは、GoogleがDagger上に構築したライブラリで、Androidアプリ開発に最適化された依存性注入を簡単に実現できます。この記事では、私がHiltを使って開発してきた実際の経験を元に、基本的な使い方から実践的なTipsまで、コード例を交えながら詳しく解説していきます。
Hiltとは何か?
Hilt は、Androidアプリケーション専用に設計された依存性注入ライブラリです。
Daggerをベースに構築されていますが、Androidアプリ開発に特化したことで、従来の複雑さを大幅に軽減しています。
Hiltの主な特徴
- ボイラープレートコードの大幅削減: 手動DI実装時のコード量を約70%削減
- Androidライフサイクル完全対応: Activity、ViewModelなどの生成・破棄タイミングを自動管理
- コンパイル時の安全性: 実行前にDI設定のエラーを検出
- 標準化されたアプローチ: チーム開発で一貫したDIパターンを実現
基本的な使い方
1. Applicationクラス
Hiltを使用する上で最初に必要な設定です。アプリケーション全体のDIコンテナを初期化し、全てのDI管理の起点となります。
@HiltAndroidApp
class MyApplication : Application()
@HiltAndroidApp
アノテーションによって、Hiltが自動的にアプリケーションレベルの依存関係コンテナを生成します。これにより、シングルトンスコープのオブジェクトやアプリケーション全体で使用される依存関係が管理されるようになります。
AndroidManifest.xmlでアプリケーションクラスを指定:
<application
android:name=".MyApplication"
...>
2. Activity
ActivityでHiltを使用する場合は、@AndroidEntryPoint
アノテーションを付けることで依存関係の注入が可能になります。これによりActivity内で@Inject
アノテーションを使って必要な依存関係を自動的に受け取ることができます。
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
@Inject
lateinit var userRepository: UserRepository
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// userRepositoryが自動的に注入されます(魔法みたい!)
userRepository.getCurrentUser()
}
}
@AndroidEntryPoint
は、Service、BroadcastReceiverなど他のAndroidコンポーネントでも同様に使用できます。ActivityのライフサイクルメソッドよりもHiltの注入が先に実行されるため、onCreate()
メソッド内で安全に依存関係を使用できます。
ViewModelの使い方
Hiltの真価が最も発揮されるのがViewModelです。従来のViewModelFactoryを作る必要がなくなり、コード量が劇的に減ります。
1. ViewModel
@HiltViewModel
アノテーションを付けることで、ViewModelでも依存関係の注入が可能になります。従来必要だったViewModelFactoryの実装が不要になり、コンストラクタで直接依存関係を受け取ることができます。
まずは基本的なパターンから見てみましょう:
@HiltViewModel
class UserProfileViewModel @Inject constructor(
private val userRepository: UserRepository,
private val analyticsService: AnalyticsService
) : ViewModel() {
private val _userProfile = MutableLiveData<User>()
val userProfile: LiveData<User> = _userProfile
fun loadUserProfile(userId: String) {
viewModelScope.launch {
try {
val user = userRepository.getUserById(userId)
_userProfile.value = user
analyticsService.trackEvent("profile_loaded")
} catch (e: Exception) {
// エラーハンドリング
}
}
}
}
2. Activity
ActivityでHiltViewModelを使用する場合は、by viewModels()
デリゲートを使ってViewModelのインスタンスを取得します。ViewModelFactoryを明示的に指定する必要がなく、Hiltが自動的に依存関係を解決してViewModelを提供してくれます。
@AndroidEntryPoint
class UserProfileActivity : AppCompatActivity() {
// たったこれだけで依存関係が解決される!
private val viewModel: UserProfileViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
viewModel.userProfile.observe(this) { user ->
// UIの更新処理
}
viewModel.loadUserProfile("user123")
}
}
by viewModels()
はActivity-KTXライブラリで提供されるデリゲートで、Activityのライフサイクルに紐づいたViewModelインスタンスを自動的に管理してくれます。Configuration changeが発生してもViewModelは保持されます。
DIなしの時代と比べてみると…
HiltによるDIの恩恵を理解するために、従来の手動で依存関係を管理していた時代と比較してみましょう。依存関係が複雑になるほど、手動管理の煩雑さとHiltのメリットが明確に現れます。
// DIなしの時代(悪夢のような依存関係の手動管理)
class UserProfileActivity : AppCompatActivity() {
private lateinit var viewModel: UserProfileViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// 依存関係を手動で作成…つらい😭
val apiService = ApiService.create()
val database = AppDatabase.getInstance(this)
val userDao = database.userDao()
val preferencesManager = PreferencesManager(this)
val userRepository = UserRepositoryImpl(apiService, userDao, preferencesManager)
val analyticsService = AnalyticsService(this)
val factory = UserProfileViewModelFactory(userRepository, analyticsService)
viewModel = ViewModelProvider(this, factory)[UserProfileViewModel::class.java]
// やっとViewModelが使える状態に…
}
}
この違い、めちゃくちゃ大きいですよね!
Hiltモジュールの作成
ここからがHiltの本領発揮です。依存関係をどう提供するかを定義していきます。
インターフェースのバインド
抽象クラスやインターフェースの実装を具体的なクラスに関連付ける場合は@Binds
アノテーションを使用します。これにより、インターフェースを依存関係として要求した際に、自動的に指定された実装クラスのインスタンスが提供されます。
抽象クラスやインターフェースの実装をバインドする時は@Binds
を使います:
@Module
@InstallIn(SingletonComponent::class)
abstract class RepositoryModule {
@Binds
abstract fun bindUserRepository(
userRepositoryImpl: UserRepositoryImpl
): UserRepository
}
インスタンスの提供
コンストラクタに@Inject
を付けられない場合や、複雑な初期化処理が必要な場合は@Provides
アノテーションを使用してインスタンスを提供します。特にサードパーティライブラリやビルダーパターンを使うオブジェクトの生成に適しています。
具体的なオブジェクトを作成して提供する場合は@Provides
を使います。ネットワーク関連の設定はこのパターンが多いですね:
@Module
@InstallIn(SingletonComponent::class)
object NetworkModule {
@Provides
@Singleton
fun provideOkHttpClient(): OkHttpClient {
return OkHttpClient.Builder()
.addInterceptor(HttpLoggingInterceptor().apply {
level = HttpLoggingInterceptor.Level.BODY
})
.connectTimeout(30, TimeUnit.SECONDS)
.readTimeout(30, TimeUnit.SECONDS)
.build()
}
@Provides
@Singleton
fun provideRetrofit(client: OkHttpClient): Retrofit {
return Retrofit.Builder()
.baseUrl("https://api.example.com/")
.client(client)
.addConverterFactory(GsonConverterFactory.create())
.build()
}
@Provides
@Singleton
fun provideApiService(retrofit: Retrofit): ApiService {
return retrofit.create(ApiService::class.java)
}
}
スコープの活用
Hiltの重要な概念の一つがスコープです。オブジェクトをいつ作成し、いつ破棄するかを制御できます。
使用可能なスコープ一覧
最初はこれだけ覚えておけばOKです:
-
@Singleton
: アプリ全体で単一インスタンス -
@ActivityScoped
: Activity のライフサイクルに合わせる -
@ActivityRetainedScoped
: Configuration changes をまたいで保持 -
@ServiceScoped
: Service のライフサイクルに合わせる -
@ViewModelScoped
: ViewModel のライフサイクルに合わせる
スコープの実践例
実際の使い分けを見てみましょう:
@Module
@InstallIn(ActivityComponent::class)
object ActivityModule {
@ActivityScoped
@Provides
fun provideLocationTracker(activity: Activity): LocationTracker {
return LocationTracker(activity)
}
}
@ActivityScoped
class LocationTracker @Inject constructor(
private val activity: Activity
) {
// Activityのライフサイクルに合わせて管理される
}
Jetpack Composeとの連携
HiltはJetpack Composeとの相性も抜群です。hiltViewModel()
を使うだけで、Composable内でViewModelを簡単に取得できます。
Compose画面での使用
Jetpack ComposeとHiltの組み合わせは、宣言的UIと依存関係注入の両方のメリットを活用できる強力な組み合わせです。hiltViewModel()
関数を使用することで、Composable関数内で簡単にViewModelを取得できます。
@AndroidEntryPoint
class ComposeActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
MyAppTheme {
UserProfileScreen()
}
}
}
}
@Composable
fun UserProfileScreen(
viewModel: UserProfileViewModel = hiltViewModel()
) {
val userProfile by viewModel.userProfile.observeAsState()
LaunchedEffect(Unit) {
viewModel.loadUserProfile("user123")
}
userProfile?.let { user ->
Column {
Text(text = user.name)
Text(text = user.email)
}
}
}
WorkManagerとの連携
HiltはWorkManagerとの連携も素晴らしいです。バックグラウンド処理でも依存関係を自動注入できます。
HiltWorkerの実装
WorkManagerとHiltを組み合わせることで、バックグラウンド処理でも依存関係の注入が可能になります。@HiltWorker
と@AssistedInject
アノテーションを使用して、WorkerクラスでもRepositoryやServiceなどの依存関係を自動で受け取ることができます。
@AssistedInject
を使ってWorkerを作成します:
@HiltWorker
class DataSyncWorker @AssistedInject constructor(
@Assisted context: Context,
@Assisted params: WorkerParameters,
private val userRepository: UserRepository,
private val analyticsService: AnalyticsService
) : CoroutineWorker(context, params) {
override suspend fun doWork(): Result {
return try {
userRepository.syncUserData()
analyticsService.trackEvent("data_sync_success")
Result.success()
} catch (e: Exception) {
analyticsService.trackEvent("data_sync_failed")
Result.failure()
}
}
@AssistedFactory
interface Factory {
fun create(context: Context, params: WorkerParameters): DataSyncWorker
}
}
WorkManagerの設定
HiltWorkerを使用するためには、カスタムWorkerFactoryをWorkManagerに設定する必要があります。アプリケーションクラスでConfiguration.Provider
を実装し、Hilt用のWorkerFactoryを提供することで、WorkManagerがHiltで管理された依存関係を正しく注入できるようになります。
@Module
@InstallIn(SingletonComponent::class)
object WorkerModule {
@Provides
@Singleton
fun provideWorkerFactory(
dataSyncWorkerFactory: DataSyncWorker.Factory
): HiltWorkerFactory {
return HiltWorkerFactory(mapOf(
DataSyncWorker::class.java to dataSyncWorkerFactory
))
}
}
@HiltAndroidApp
class MyApplication : Application(), Configuration.Provider {
@Inject
lateinit var workerFactory: HiltWorkerFactory
override fun getWorkManagerConfiguration(): Configuration {
return Configuration.Builder()
.setWorkerFactory(workerFactory)
.build()
}
}
最後に
Hiltを効果的に使うための私なりのコツ
- 小さく始める: まずはRepositoryとViewModelから導入
- スコープを意識する: オブジェクトのライフサイクルをしっかり設計
- モジュールを適切に分割: 機能ごと・レイヤーごとにモジュールを作成
-
テストファーストで考える:
@TestInstallIn
でテスト用実装を準備
実際にHiltを導入して感じたメリット
- コード量の激減:設定ファイルが従来の半分以下に
- 新メンバーの学習コストが大幅削減:Daggerと比べて理解しやすい
- テストが書きやすい:モックの注入が簡単
- ビルド時間短縮:KSP使用で毎日のストレスが軽減
私はもうHiltなしのAndroid開発には戻れません😊 まだ使ったことがない方は、ぜひ小さなプロジェクトから試してみてください。きっと開発体験が大きく向上するはずです!