Files
vnidrop/.codex/skills/compose-skill/references/navigation-2-di.md
Hammed Abass fd4fff2c86 docs: add compose development guidelines and skills
Added a comprehensive collection of Compose development guidelines, best practices, and skill definitions in the .codex directory.

Co-authored-by: Junie <junie@jetbrains.com>
2026-07-07 16:55:43 +02:00

4.6 KiB

Navigation 2 + Dependency Injection

DI wiring for Nav 2 destinations: destination-scoped and graph-scoped ViewModels with Hilt and Koin.

For Nav 2 core reference (NavHost, tabs, deep links, animations), see navigation-2.md. For shared navigation concepts and anti-patterns, see navigation.md.

Hilt Integration

hiltViewModel in composable destinations

Each composable() destination gets its own ViewModel instance scoped to the NavBackStackEntry:

composable<Detail> { backStackEntry ->
    val viewModel = hiltViewModel<DetailViewModel>()
    DetailScreen(viewModel = viewModel)
}

SavedStateHandle for navigation arguments

Hilt auto-injects SavedStateHandle populated with navigation arguments. The ViewModel receives route params without manual extraction:

@HiltViewModel
class DetailViewModel @Inject constructor(
    private val repository: ItemRepository,
    savedStateHandle: SavedStateHandle,
) : ViewModel() {
    private val itemId: String = checkNotNull(savedStateHandle["itemId"])
}

Graph-scoped shared ViewModel

Share a ViewModel across all destinations within a nested navigation graph (e.g., a multi-step checkout flow):

composable("checkout/cart") { entry ->
    val parentEntry = remember(entry) { navController.getBackStackEntry("checkout") }
    val sharedViewModel: CheckoutViewModel = hiltViewModel(parentEntry)
    CartScreen(viewModel = sharedViewModel)
}

All destinations in the checkout graph share the same CheckoutViewModel instance, which is cleared when the graph is popped from the back stack.

@AssistedInject for non-navigation params

When a ViewModel needs values that aren't in navigation arguments and can't go through SavedStateHandle:

@HiltViewModel(assistedFactory = EditorViewModel.Factory::class)
class EditorViewModel @AssistedInject constructor(
    private val repository: DocRepository,
    @Assisted private val mode: EditMode,
) : ViewModel() {

    @AssistedFactory
    interface Factory {
        fun create(mode: EditMode): EditorViewModel
    }
}

// Composable destination
composable<Editor> {
    val viewModel = hiltViewModel<EditorViewModel, EditorViewModel.Factory> { factory ->
        factory.create(EditMode.CREATE)
    }
}

Prefer SavedStateHandle for navigation arguments (simpler, survives process death). Use @AssistedInject only when SavedStateHandle can't carry the data.

Koin Integration

koinViewModel in composable destinations

Standard ViewModel injection using koinViewModel():

composable<Detail> {
    val detail: Detail = it.toRoute()
    DetailScreen(viewModel = koinViewModel { parametersOf(detail.itemId) })
}

koinNavViewModel — auto-populated SavedStateHandle

koinNavViewModel() automatically populates the ViewModel's SavedStateHandle with navigation arguments. The ViewModel receives route params via its constructor without manual extraction:

class DetailViewModel(
    private val repository: ItemRepository,
    savedStateHandle: SavedStateHandle,
) : ViewModel() {
    private val itemId: String = checkNotNull(savedStateHandle["itemId"])
}

// Module declaration
val featureModule = module {
    viewModelOf(::DetailViewModel)
}

// Composable destination — SavedStateHandle auto-populated with nav args
composable("detail/{itemId}") {
    val viewModel = koinNavViewModel<DetailViewModel>()
    DetailScreen(viewModel = viewModel)
}

sharedKoinViewModel — graph-scoped sharing

Share a ViewModel within a navigation graph. The shared instance lives as long as the graph's back stack entry:

navigation(startDestination = "checkout/cart", route = "checkout") {
    composable("checkout/cart") { entry ->
        val sharedVm = entry.sharedKoinViewModel<CheckoutViewModel>(navController)
        CartScreen(viewModel = sharedVm)
    }
    composable("checkout/shipping") { entry ->
        val sharedVm = entry.sharedKoinViewModel<CheckoutViewModel>(navController)
        ShippingScreen(viewModel = sharedVm)
    }
}

This is the Koin equivalent of Hilt's hiltViewModel(navController.getBackStackEntry("checkout")) pattern.

Quick reference — Koin Nav 2 injection functions

Function Purpose
koinViewModel<T>() Standard injection — new instance per destination
koinNavViewModel<T>() Like koinViewModel but auto-populates SavedStateHandle with nav arguments
sharedKoinViewModel<T>(navController) Share ViewModel within a navigation graph (experimental)
koinViewModel(parameters = { parametersOf(...) }) Pass runtime values to the ViewModel constructor