Tanpa dependency injection, kita menulis PenggunaApi(baseUrl), lalu TugasRepository(api), lalu TugasViewModel(repo) secara manual di banyak tempat. Codebase berubah menjadi rentan: bila constructor TugasRepository butuh parameter baru, semua tempat yang membuat instance harus diubah. Hilt adalah library Jetpack yang membungkus Dagger 2 — generator kode compile-time yang mengotomatiskan dependency injection khusus Android. Bab ini menjelaskan anotasi inti Hilt, struktur module, dan bagaimana ViewModel menerima dependensi tanpa boilerplate.

@HiltAndroidApp dan Component Hierarchy #

Anotasi @HiltAndroidApp dipasang di kelas Application. Hilt meng-generate container di level aplikasi yang menjadi root untuk semua binding. Selain SingletonComponent (aplikasi), Hilt menyediakan component khusus Android berikut:

ComponentScopeLifetime
SingletonComponent@SingletonSepanjang aplikasi hidup
ViewModelComponent@ViewModelScopedSelama ViewModel hidup
ActivityComponent@ActivityScopedSelama Activity hidup
FragmentComponent@FragmentScopedSelama Fragment hidup
ViewComponent@ViewScopedSelama View hidup

Pemilihan component menentukan kapan instance di-refresh. Singleton cocok untuk Retrofit, OkHttp, dan Repository. ViewModelScoped cocok untuk state yang harus bersama antar beberapa ViewModel dalam satu layar.

@Inject untuk Constructor Injection #

Anotasi @Inject pada constructor memberi tahu Hilt cara membuat instance. Saat class lain meminta instance ini, Hilt otomatis memanggil constructor dengan dependensi yang sesuai.

kotlin Constructor injection
class TugasRepository @Inject constructor(
    private val api: PenggunaApi,
    private val dao: TugasDao
) { /* ... */ }

class TugasViewModel @Inject constructor(
    private val repo: TugasRepository
) : ViewModel() { /* ... */ }

Hilt meng-generate factory yang tahu cara membuat TugasViewModel — kita hanya perlu memanggil viewModel<TugasViewModel>() di Compose. Bila suatu hari TugasRepository butuh parameter baru, hanya satu tempat yang berubah (factory Hilt).

@Module dan @InstallIn #

Untuk class yang tidak kita miliki (mis. Retrofit, OkHttpClient, RoomDatabase), kita tidak bisa menambah @Inject. Solusinya: definisikan binding di @Module dengan method @Provides:

kotlin Module dengan @Provides
@Module
@InstallIn(SingletonComponent::class)
object NetworkModule {
    @Provides
    @Singleton
    fun provideOkHttp(): OkHttpClient = OkHttpClient.Builder()
        .addInterceptor(HttpLoggingInterceptor().apply { level = BODY })
        .build()

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

    @Provides
    @Singleton
    fun providePenggunaApi(retrofit: Retrofit): PenggunaApi =
        retrofit.create(PenggunaApi::class.java)
}

@InstallIn(SingletonComponent::class) menentukan bahwa binding di module ini hidup selama aplikasi hidup. Setiap method @Provides menjadi satu titik binding — Hilt otomatis memanggilnya saat instance dibutuhkan.

@Inject di ViewModel #

Compose ViewModel diakses lewat viewModel<TugasViewModel>(). Hilt menyediakan factory yang tahu cara membuat instance dengan constructor injection.

kotlin Pakai ViewModel di Compose
@HiltViewModel
class TugasViewModel @Inject constructor(
    private val repo: TugasRepository
) : ViewModel() { /* ... */ }

@Composable
fun TugasScreen(vm: TugasViewModel = viewModel()) {
    val state by vm.state.collectAsState()
    // ...
}

Simulasi Hilt Container #

Cuplikan berikut memodelkan Hilt container sebagai registry yang menyimpan factory dan instance. Anotasi @HiltAndroidApp, @Inject, @Module, @InstallIn, dan @Provides disimulasikan sebagai anotasi biasa:

kotlin
// dependency_injection_hilt.kt
// Mendemonstrasikan Hilt sebagai wrapper Dagger untuk Android: @HiltAndroidApp,
// @Inject, @Module, @InstallIn, dan @Provides. Disimulasikan sebagai registry
// yang menyimpan binding.

// Annotation simulasi
@Target(AnnotationTarget.CLASS) annotation class HiltAndroidApp
@Target(AnnotationTarget.CONSTRUCTOR) annotation class Inject
@Target(AnnotationTarget.CLASS) annotation class Module
@Target(AnnotationTarget.CLASS) annotation class InstallIn(val scope: String)
@Target(AnnotationTarget.FUNCTION) annotation class Provides

// Komponen aplikasi
open class ApplicationContext(val packageName: String)
open class PenggunaApi(val baseUrl: String)
open class TugasRepository(val api: PenggunaApi)
open class TugasViewModel(val repo: TugasRepository)

// Komponen registry (mensimulasikan Hilt component container)
class HiltContainer {
    private val singletons = mutableMapOf<String, Any>()
    private val factories = mutableMapOf<String, () -> Any>()

    fun <T : Any> singleton(key: String, factory: () -> T) {
        factories[key] = factory
    }

    @Suppress("UNCHECKED_CAST")
    fun <T : Any> get(key: String): T {
        if (!singletons.containsKey(key)) {
            val factory = factories[key] ?: error("Binding untuk $key tidak ditemukan")
            singletons[key] = factory()
        }
        return singletons[key] as T
    }

    fun dumpBindings(): List<String> = factories.keys.toList()
}

// Module: ApplicationModule (instal di SingletonComponent)
@Module
@InstallIn("SingletonComponent")
class ApplicationModule {
    @Provides
    fun provideContext(): ApplicationContext = ApplicationContext("com.example.tugasapp")

    @Provides
    fun provideApi(): PenggunaApi = PenggunaApi("https://api.example.com")
}

// Module: RepositoryModule (instal di SingletonComponent)
@Module
@InstallIn("SingletonComponent")
class RepositoryModule {
    @Provides
    fun provideTugasRepository(api: PenggunaApi): TugasRepository = TugasRepository(api)
}

// @HiltAndroidApp — kelas Application yang menjadi root container
@HiltAndroidApp
class TugasApplication

// ViewModel dengan constructor injection
class TugasViewModelImpl @Inject constructor(
    repo: TugasRepository
) : TugasViewModel(repo)

fun main() {
    val container = HiltContainer()

    // Mendaftarkan provider dari module (mensimulasikan generated code Hilt)
    val appModule = ApplicationModule()
    val repoModule = RepositoryModule()

    container.singleton("ApplicationContext") { appModule.provideContext() }
    container.singleton("PenggunaApi") { appModule.provideApi() }
    container.singleton("TugasRepository") { repoModule.provideTugasRepository(container.get("PenggunaApi")) }
    container.singleton("TugasViewModel") { TugasViewModelImpl(container.get<TugasRepository>("TugasRepository")) }

    println("=== Bindings terdaftar ===")
    container.dumpBindings().forEach { println("  - $it") }

    println("\n=== Resolve dependencies ===")
    val vm: TugasViewModelImpl = container.get("TugasViewModel")
    println("ViewModel.repo.api.baseUrl = ${vm.repo.api.baseUrl}")

    println("\n=== Singleton: instance sama ===")
    val vm2: TugasViewModelImpl = container.get("TugasViewModel")
    println("vm === vm2: ${vm === vm2}")
    println("vm.repo === vm2.repo: ${vm.repo === vm2.repo}")
}

Output yang Dihasilkan:

text
=== Bindings terdaftar ===
  - ApplicationContext
  - PenggunaApi
  - TugasRepository
  - TugasViewModel

=== Resolve dependencies ===
ViewModel.repo.api.baseUrl = https://api.example.com

=== Singleton: instance sama ===
vm === vm2: true
vm.repo === vm2.repo: true

Perhatikan bahwa vm === vm2 bernilai true — Singleton menyimpan instance yang sama. Begitu pula vm.repo === vm2.repo — Repository juga Singleton. Inilah inti DI: instance di-share sesuai scope yang dipilih.

Referensi Lanjutan #

Selanjutnya #

Dengan DI terstruktur, kita siap membahas permission — hak akses sistem seperti kamera, lokasi, dan notifikasi yang harus diminta saat runtime. Bab Permissions dan Runtime Permissions akan membahas requestPermission() dan rememberLauncherForActivityResult().