Hilt Dependency Injection in Jetpack Compose

Quick answer: Let Hilt construct repositories and other app dependencies, inject them into an
@HiltViewModelconstructor, then obtain that ViewModel at a screen-level composable withhiltViewModel(). Pass plain state and event callbacks down to the UI instead of injecting dependencies into every child.
Hilt manages object lifetimes; Compose renders state. Keeping those jobs separate produces a small, testable boundary: a route composable obtains the ViewModel, while a stateless screen displays its uiState and reports events. Android’s Hilt guide and Compose integration guide use this constructor-injected ViewModel pattern.
Configure Hilt with KSP
The following current setup uses Hilt 2.57.1 and KSP. Declare the Hilt Gradle plugin once at the project level, then apply it in the Android application module.
// build.gradle.kts at the project level
plugins {
id("com.google.dagger.hilt.android") version "2.57.1" apply false
}// app/build.gradle.kts
plugins {
id("com.google.devtools.ksp")
id("com.google.dagger.hilt.android")
}
dependencies {
implementation("com.google.dagger:hilt-android:2.57.1")
ksp("com.google.dagger:hilt-android-compiler:2.57.1")
implementation("androidx.hilt:hilt-lifecycle-viewmodel-compose:1.4.0")
}The Compose hiltViewModel() API moved to androidx.hilt:hilt-lifecycle-viewmodel-compose in Hilt 1.3.0 so it no longer requires Navigation as a transitive dependency. Import it from androidx.hilt.lifecycle.viewmodel.compose; the old androidx.hilt.navigation.compose version is deprecated. Confirm the versions compatible with your project in the Hilt release notes.
Create the application entry point
Annotate the Application class so Hilt can generate the application-level component. An Activity that hosts Compose needs @AndroidEntryPoint so Hilt-aware ViewModel factories can participate in its lifecycle.
@HiltAndroidApp
class BlogApplication : Application()
@AndroidEntryPoint
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent { BlogApp() }
}
}Register BlogApplication in the manifest if it is not already your application’s class. Hilt supports Android framework entry points such as applications, activities, fragments, services, and broadcast receivers; Compose itself is not an injection target.
Bind an interface close to its lifetime
Use constructor injection whenever Hilt can create the implementation directly. Use a module to describe an interface-to-implementation binding or to construct a class you do not own.
interface ArticleRepository {
fun observeArticles(): Flow<List<Article>>
}
class NetworkArticleRepository @Inject constructor(
private val articleApi: ArticleApi,
) : ArticleRepository {
override fun observeArticles(): Flow<List<Article>> = articleApi.observe()
}
@Module
@InstallIn(SingletonComponent::class)
abstract class RepositoryModule {
@Binds
@Singleton
abstract fun bindArticleRepository(
implementation: NetworkArticleRepository,
): ArticleRepository
}ArticleApi is illustrative application code; it needs its own Hilt binding, commonly from an @Provides method that constructs a network client. Choose @Singleton only when a single process-wide instance is actually correct. A scope controls one component’s lifetime—it is not a general performance annotation.
Inject a ViewModel, then collect state at the route
Hilt can supply constructor dependencies and SavedStateHandle to an @HiltViewModel. The route collects state; the presentation composable stays free of Hilt and ViewModel types.
@HiltViewModel
class ArticleListViewModel @Inject constructor(
private val repository: ArticleRepository,
private val savedStateHandle: SavedStateHandle,
) : ViewModel() {
val uiState: StateFlow<ArticleListUiState> = repository
.observeArticles()
.map { articles -> ArticleListUiState.Content(articles) }
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5_000),
initialValue = ArticleListUiState.Loading,
)
fun onRetry() {
// Request a refresh from the repository.
}
}
@Composable
fun ArticleListRoute(
viewModel: ArticleListViewModel = hiltViewModel(),
) {
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
ArticleListScreen(
uiState = uiState,
onRetry = viewModel::onRetry,
)
}The required import is androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel. hiltViewModel() returns a ViewModel scoped to the current ViewModelStoreOwner; when called inside a Navigation Compose destination, that is normally the destination’s back-stack entry. Collecting the StateFlow at the route keeps lifecycle work out of reusable UI. See collectAsStateWithLifecycle with StateFlow for that boundary in detail.
Scope shared state to a parent navigation graph
Two destinations in one flow sometimes need one shared ViewModel—for example, a multi-step checkout or sign-in graph. Pass the parent NavBackStackEntry explicitly to hiltViewModel() rather than creating separate instances in each child destination.
NavHost(navController, startDestination = "auth") {
navigation(
route = "auth",
startDestination = "auth/email",
) {
composable("auth/email") { backStackEntry ->
val parentEntry = remember(backStackEntry) {
navController.getBackStackEntry("auth")
}
val viewModel = hiltViewModel<AuthViewModel>(parentEntry)
EmailRoute(viewModel = viewModel)
}
composable("auth/verification") { backStackEntry ->
val parentEntry = remember(backStackEntry) {
navController.getBackStackEntry("auth")
}
val viewModel = hiltViewModel<AuthViewModel>(parentEntry)
VerificationRoute(viewModel = viewModel)
}
}
}The auth graph must be on the back stack before looking up its entry. Scope sharing to the smallest graph that owns the shared state, and remove that graph after completion if Back must not revisit the flow. Nested Navigation Graphs in Jetpack Compose covers the route ownership and Back-stack side.
Keep injected objects out of leaf composables
Avoid this boundary:
@Composable
fun ArticleRow() {
val viewModel: ArticleListViewModel = hiltViewModel()
// A small reusable row now knows about an app-level state owner.
}Instead, let a route or screen-level container own the ViewModel and pass the smallest state plus event callbacks to ArticleRow. This makes previews practical, lets unit and UI tests provide fakes easily, and avoids accidentally changing a ViewModel’s owner as composition moves.
Test the boundaries
- Unit-test a ViewModel by constructing it with a fake
ArticleRepositoryand a testSavedStateHandle; Hilt is not required for that test. - Test
ArticleListScreenwith fixedArticleListUiStatevalues and callbacks that record the event. - Use a Hilt integration test only when you need to verify real bindings, scopes, or a production-like entry point. Hilt’s testing guide documents the separate test processor dependencies.
- Test navigation actions separately from DI wiring. Testing Navigation Compose shows
TestNavHostControllerand UI-driven graph assertions.
Common mistakes
Using the deprecated Navigation Compose import
androidx.hilt.navigation.compose.hiltViewModel still appears in older examples, but its API is deprecated. Use androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel and the lifecycle ViewModel Compose artifact for new code.
Injecting an Activity or view into a ViewModel
ViewModels should not hold UI objects or a reference back to an Activity. Inject a repository or other business dependency; use @ApplicationContext only when an application context is truly needed.
Making everything a singleton
Singleton scope can hide state leaks and makes replacement harder in tests. Match the scope to the dependency’s real lifetime, and leave stateless objects unscoped unless a shared instance is meaningful.
Treating Hilt as a screen-state architecture
Hilt creates objects; it does not define UI state, events, loading, retries, or effects. Continue to model those explicitly in the ViewModel and UI boundary.
FAQ
Do I need hilt-navigation-compose for a Navigation Compose app?
Not to call the current hiltViewModel() API. It now lives in hilt-lifecycle-viewmodel-compose and can receive the current or an explicitly supplied NavBackStackEntry as its owner.
Where should hiltViewModel() be called?
At a route or screen-level composable, close to the NavHost destination. Pass state and callbacks below it rather than a ViewModel instance into every UI component.
Can Hilt inject a composable function directly?
No. Hilt manages supported Android entry points and object graphs. Compose functions receive dependencies through parameters, typically from an injected ViewModel at the screen boundary.