Compile-time DI for Android-only Compose projects with ViewModel and lifecycle integration.
For Hilt vs Koin decision guidance and shared DI concepts, see dependency-injection.md. For Koin (multiplatform), see koin.md.
References:
// project-level build.gradle.kts
plugins {
alias(libs.plugins.hilt) apply false
}
// app-level build.gradle.kts
plugins {
alias(libs.plugins.android.application)
alias(libs.plugins.kotlin.android)
alias(libs.plugins.hilt)
alias(libs.plugins.ksp)
}
dependencies {
implementation(libs.hilt.android)
ksp(libs.hilt.compiler)
// Compose integration
implementation(libs.hilt.navigation.compose)
}
@HiltAndroidApp
class MyApplication : Application()
Every Hilt app requires an @HiltAndroidApp-annotated Application class.
Use for third-party classes, builder patterns, or anything where you control creation logic:
@Module
@InstallIn(SingletonComponent::class)
object AppModule {
@Provides
@Singleton
fun provideApiClient(): ApiClient = ApiClient()
@Provides
@Singleton
fun provideDatabase(@ApplicationContext context: Context): AppDatabase =
Room.databaseBuilder(context, AppDatabase::class.java, "app.db").build()
}
Use for interface-to-implementation bindings. More efficient than @Provides (no method body needed, generates less code):
@Module
@InstallIn(SingletonComponent::class)
abstract class RepositoryModule {
@Binds
@Singleton
abstract fun bindUserRepository(impl: UserRepositoryImpl): UserRepository
@Binds
@Singleton
abstract fun bindProductRepository(impl: ProductRepositoryImpl): ProductRepository
}
Use ViewModelComponent when dependencies are only needed within a ViewModel and should be cleaned up when the ViewModel is cleared. Use SingletonComponent for app-wide shared instances (API clients, databases).
@Module
@InstallIn(ViewModelComponent::class)
object ProductModule {
@Provides
@ViewModelScoped
fun provideProductCalculator(): ProductCalculator = ProductCalculator()
@Provides
@ViewModelScoped
fun provideProductValidator(): ProductValidator = ProductValidator()
}
@HiltViewModel
class ProductViewModel @Inject constructor(
private val calculator: ProductCalculator,
private val repository: ProductRepository,
) : ViewModel() {
// StateFlow<State>, Channel<Effect>, onEvent() — see architecture.md
}
Hilt auto-injects SavedStateHandle populated with navigation arguments. Use when the ViewModel receives serializable route params:
@HiltViewModel
class DetailViewModel @Inject constructor(
private val repository: ItemRepository,
savedStateHandle: SavedStateHandle,
) : ViewModel() {
private val itemId: String = checkNotNull(savedStateHandle["itemId"])
init {
loadItem(itemId)
}
}
Use when the ViewModel needs values that aren't in navigation arguments (e.g., a complex object, a callback, or a value computed in the composable):
@HiltViewModel(assistedFactory = DetailViewModel.Factory::class)
class DetailViewModel @AssistedInject constructor(
private val repository: ItemRepository,
@Assisted private val itemId: String,
) : ViewModel() {
@AssistedFactory
interface Factory {
fun create(itemId: String): DetailViewModel
}
}
// Caller passes the value explicitly
@Composable
fun DetailRoute(itemId: String) {
val viewModel = hiltViewModel<DetailViewModel, DetailViewModel.Factory> { factory ->
factory.create(itemId)
}
}
Prefer SavedStateHandle for navigation arguments (simpler, survives process death). Use @AssistedInject only when SavedStateHandle can't carry the data.
@AndroidEntryPoint
class MainActivity : ComponentActivity() { /* setContent { ... } */ }
@Composable
fun ProductRoute(viewModel: ProductViewModel = hiltViewModel()) {
val state by viewModel.state.collectAsStateWithLifecycle()
ProductScreen(state = state, onEvent = viewModel::onEvent)
}
Every Activity hosting Hilt-injected composables requires @AndroidEntryPoint. Use the standard MVI Route/Screen pattern: collect state via collectAsStateWithLifecycle(), collect effects via CollectEffect, pass onEvent to Screen.
For Nav 3 + Hilt patterns (entry-scoped ViewModels, multibinding entry providers), see navigation-3-di.md — that is the preferred approach for new projects. For Nav 2 + Hilt patterns (graph-scoped VMs, @AssistedInject), see navigation-2-di.md.
The patterns below apply to Navigation Compose (Nav 2) projects that use Hilt. They remain valid for existing codebases but should not be the starting point for new work.
Use hiltViewModel() as a default parameter in any composable() destination — each destination gets its own ViewModel instance scoped to the NavBackStackEntry.
Use when destinations within the same Nav 2 navigation graph need a shared ViewModel (e.g., a multi-step checkout flow where Cart, Shipping, and Payment screens share CheckoutViewModel):
val parentEntry = remember(navController) {
navController.getBackStackEntry("checkout_graph")
}
val sharedViewModel: CheckoutViewModel = hiltViewModel(parentEntry)
| Scope | Lifecycle | Use case |
|---|---|---|
@Singleton |
Application | API clients, databases, shared preferences |
@ActivityRetainedScoped |
Activity (survives config change) | User session, auth state |
@ViewModelScoped |
ViewModel | Feature-specific services, calculators |
@ActivityScoped |
Activity instance | Activity-bound resources |
@FragmentScoped |
Fragment instance | Fragment-bound resources (rare in Compose) |
The only Hilt-specific wiring is @HiltViewModel + @Inject constructor. The MVI pattern (Event/State/Effect, onEvent()) is framework-agnostic — DI only affects constructor injection and injection-site calls.
For ViewModel unit tests (no Hilt needed), see testing.md.
dependencies {
androidTestImplementation(libs.hilt.android.testing)
kspAndroidTest(libs.hilt.compiler)
}
@HiltAndroidTest
class CreateItemScreenTest {
@get:Rule(order = 0)
val hiltRule = HiltAndroidRule(this)
@get:Rule(order = 1)
val composeRule = createAndroidComposeRule<MainActivity>()
@Inject
lateinit var repository: ItemRepository
@Before
fun setup() {
hiltRule.inject()
}
@Test
fun saveButton_enabledWhenFieldsFilled() {
composeRule.setContent {
CreateItemScreen(
state = CreateItemState(title = "Test", amount = "100"),
onEvent = {},
)
}
composeRule.onNodeWithText("Save").assertIsEnabled()
}
}
@Module
@InstallIn(SingletonComponent::class)
@TestInstallIn(components = [SingletonComponent::class], replaces = [RepositoryModule::class])
object FakeRepositoryModule {
@Provides
@Singleton
fun provideItemRepository(): ItemRepository = FakeItemRepository()
}
| Anti-pattern | Why it is harmful | Better approach |
|---|---|---|
| Injecting Context into ViewModel | Lifecycle mismatch, leaks | Use @ApplicationContext or move platform code to Repository |
| Injecting Activity/Fragment into ViewModel | Memory leaks | Pass data via SavedStateHandle or route arguments |
@Inject on ViewModel without @HiltViewModel |
ViewModel not managed by Hilt | Always use @HiltViewModel with @Inject constructor |
| Manual ViewModel instantiation | Bypasses Hilt injection | Use hiltViewModel() in Compose |
Installing ViewModel dependencies in SingletonComponent |
Unnecessary lifecycle extension | Use ViewModelComponent or ViewModelScoped |