2
2

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開発を加速させるHilt徹底解説:ViewModel、Compose、WorkManager連携まで

Posted at

はじめに

こんにちは!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を効果的に使うための私なりのコツ

  1. 小さく始める: まずはRepositoryとViewModelから導入
  2. スコープを意識する: オブジェクトのライフサイクルをしっかり設計
  3. モジュールを適切に分割: 機能ごと・レイヤーごとにモジュールを作成
  4. テストファーストで考える: @TestInstallInでテスト用実装を準備

実際にHiltを導入して感じたメリット

  • コード量の激減:設定ファイルが従来の半分以下に
  • 新メンバーの学習コストが大幅削減:Daggerと比べて理解しやすい
  • テストが書きやすい:モックの注入が簡単
  • ビルド時間短縮:KSP使用で毎日のストレスが軽減

私はもうHiltなしのAndroid開発には戻れません😊 まだ使ったことがない方は、ぜひ小さなプロジェクトから試してみてください。きっと開発体験が大きく向上するはずです!


参考資料

2
2
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
2
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?