tech

AndroidのDI入門:Hiltの基本的な使い方

· 13min · Android, Kotlin, Hilt, DI
index

はじめに

Android開発でDI(Dependency Injection)を導入したいとき、公式が推奨しているのがHiltです。 HiltはDaggerをAndroid向けにシンプル化したライブラリで、セットアップがかなり楽になっています。

本記事では、HiltをAndroidアプリに導入するための基本的な流れをまとめます。

依存関係

// プロジェクトレベル build.gradle.kts
plugins {
    id("com.google.dagger.hilt.android") version "2.48" apply false
}

// アプリレベル build.gradle.kts
plugins {
    id("kotlin-kapt")
    id("com.google.dagger.hilt.android")
}

dependencies {
    implementation("com.google.dagger:hilt-android:2.48")
    kapt("com.google.dagger:hilt-android-compiler:2.48")
}

kaptではなくkspを使いたい場合は、Hiltがkspに対応次第乗り換えることができます。

アプリケーションクラスの設定

Hiltを使うには、Applicationクラスに@HiltAndroidAppを付けます。

@HiltAndroidApp
class MyApp : Application()

AndroidManifest.xmlにも登録を忘れずに。

<application android:name=".MyApp" ... />

ActivityとFragmentで使う

@AndroidEntryPoint
class MainActivity : ComponentActivity() { /* ... */ }

@AndroidEntryPointを付けると、そのクラスでフィールドインジェクションやViewModelの注入ができるようになります。

ViewModelへのインジェクション

@HiltViewModel
class UserViewModel @Inject constructor(
    private val repository: UserRepository,
) : ViewModel() { /* ... */ }

Activity/Fragmentからはおなじみのby viewModels()で取得できます。

class MainActivity : ComponentActivity() {
    private val viewModel: UserViewModel by viewModels()
}

Moduleで依存を提供する

インターフェースの実装や外部ライブラリのインスタンスなど、@Inject constructorで解決できないものは@Moduleで提供します。

@Module
@InstallIn(SingletonComponent::class)
object NetworkModule {

    @Provides
    @Singleton
    fun provideRetrofit(): Retrofit =
        Retrofit.Builder()
            .baseUrl("https://api.example.com")
            .addConverterFactory(MoshiConverterFactory.create())
            .build()

    @Provides
    @Singleton
    fun provideUserApi(retrofit: Retrofit): UserApi =
        retrofit.create(UserApi::class.java)
}
  • @InstallIn(SingletonComponent::class):アプリ全体でシングルトンとして共有
  • @Provides:インスタンス生成方法を示す
  • @Singleton:スコープ内で単一インスタンス

Interfaceの実装を束ねる

Repositoryなど、インターフェースを実装するクラスがある場合は@Bindsを使います。

interface UserRepository {
    suspend fun fetch(id: Long): User
}

class UserRepositoryImpl @Inject constructor(
    private val api: UserApi,
) : UserRepository {
    override suspend fun fetch(id: Long): User = api.getUser(id)
}

@Module
@InstallIn(SingletonComponent::class)
abstract class RepositoryModule {
    @Binds
    @Singleton
    abstract fun bindUserRepository(impl: UserRepositoryImpl): UserRepository
}

@Binds@Providesより最適化が効くので、単にインターフェースをバインドするだけなら@Bindsを選びます。

Scopeの使い分け

Hiltで代表的なスコープは以下。

Scopeライフサイクル
@SingletonApplication全体
@ActivityRetainedScoped画面回転を跨いで保持
@ActivityScopedActivityごと
@ViewModelScopedViewModelごと
@FragmentScopedFragmentごと

Repositoryは基本@Singleton、Use Caseは@ViewModelScoped、のように使い分けるとシンプルです。

Composeへの注入

JetpackComposeではhiltViewModel()が用意されています。

@Composable
fun UserScreen(viewModel: UserViewModel = hiltViewModel()) {
    val state by viewModel.state.collectAsStateWithLifecycle()
    // UI
}

よくあるハマりどころ

  • @HiltAndroidAppの付け忘れ:AppクラスでのDIが動かない
  • AndroidManifestへの登録忘れ:同上
  • @AndroidEntryPointを付け忘れる:ViewModelのinjectができない
  • Moduleの@InstallInが間違い:スコープ不一致エラー

Hiltはエラーメッセージがかなり親切なので、コンパイルエラーをよく読むと解決できるケースがほとんどです。

おわりに

Hiltはセットアップさえ乗り越えれば、DaggerほどのボイラープレートがなくDIを導入できます。 Androidでテスタブルな設計にしたい場合、個人的には真っ先に入れるライブラリのひとつです。

参考文献