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:
| Component | Scope | Lifetime |
|---|---|---|
| SingletonComponent | @Singleton | Sepanjang aplikasi hidup |
| ViewModelComponent | @ViewModelScoped | Selama ViewModel hidup |
| ActivityComponent | @ActivityScoped | Selama Activity hidup |
| FragmentComponent | @FragmentScoped | Selama Fragment hidup |
| ViewComponent | @ViewScoped | Selama 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.
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:
@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.
@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:
// 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:
=== 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: truePerhatikan 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 #
- Dependency injection with Hilt — panduan resmi Hilt untuk dependency injection di aplikasi Android berbasis Kotlin
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().