Navigation 3
This page shows how to declare Navigation 3 entries in Koin modules and pass them to NavDisplay. Each feature module contributes its own screens, and the app collects them all with one entry provider. It assumes you know the Navigation 3 basics (back stack, NavDisplay, NavEntry) and ViewModel in Compose.
All APIs on this page are marked @KoinExperimentalAPI.
Setup
// shared/build.gradle.kts, or app/build.gradle.kts on Android
plugins {
kotlin("plugin.serialization")
}
commonMain.dependencies {
implementation("io.insert-koin:koin-compose-navigation3:$koin_version")
implementation("org.jetbrains.kotlinx:kotlinx-serialization-core:$serialization_version")
}
koin-compose-navigation3 targets Android, iOS, JVM desktop, JS and Wasm.
Declaring entries
Routes are @Serializable classes:
@Serializable data object HomeRoute
@Serializable data class DetailRoute(val itemId: String)
Declare one navigation<T> { } per route in your modules. The lambda is a Composable that receives the route:
import org.koin.dsl.navigation3.navigation
val homeModule = module {
viewModel<HomeViewModel>()
navigation<HomeRoute> {
HomeScreen(viewModel = koinViewModel())
}
}
val detailModule = module {
viewModel<DetailViewModel>()
navigation<DetailRoute> { route ->
DetailScreen(viewModel = koinViewModel { parametersOf(route.itemId) })
}
}
Each navigation<T> is stored as a definition qualified by the route type. The lambda runs with the Koin Scope as receiver, so you can call get<T>() in it, as well as Composable functions such as koinViewModel().
Building the entry provider
koinEntryProvider() collects every navigation<T> entry of the current scope into one provider for NavDisplay:
import org.koin.compose.navigation3.koinEntryProvider
@Composable
fun App() {
val backStack = remember { mutableStateListOf<Any>(HomeRoute) }
NavDisplay(
backStack = backStack,
onBack = { backStack.removeLastOrNull() },
entryProvider = koinEntryProvider()
)
}
With rememberNavBackStack(), your routes implement NavKey. Use koinEntryProvider<NavKey>() to match the back stack type.
From an Android Activity
On Android, two extensions on ComponentCallbacks build the provider from the Activity or Fragment scope:
| Function | Behavior |
|---|---|
entryProvider<T>() | Lazy delegate: val provider by entryProvider<Any>() |
getEntryProvider<T>() | Builds the provider now |
They resolve from the component's Koin scope: the Activity scope if it implements AndroidScopeComponent, the root scope otherwise.
Entries in a scope
Declare entries inside a scope when the screens need objects that live with that scope. With activityRetainedScope, they survive configuration changes:
class Navigator(start: Any) {
val backStack = mutableStateListOf(start)
fun goTo(route: Any) { backStack.add(route) }
fun goBack() { backStack.removeLastOrNull() }
}
val appModule = module {
activityRetainedScope {
scoped { Navigator(start = ConversationList) }
navigation<ConversationList> {
val navigator = get<Navigator>()
ConversationListScreen(onClick = { navigator.goTo(ConversationDetail(it)) })
}
navigation<ConversationDetail> { route ->
ConversationDetailScreen(conversationId = route.id)
}
}
}
class MainActivity : ComponentActivity(), AndroidScopeComponent {
override val scope: Scope by activityRetainedScope()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
val navigator: Navigator = get()
NavDisplay(
backStack = navigator.backStack,
onBack = { navigator.goBack() },
entryProvider = getEntryProvider()
)
}
}
}
getEntryProvider() reads the entries from the Activity scope. Entries declared in a scope are visible only from that scope. activityRetainedScope is described in Android Scopes.
ViewModels for an entry
To scope a ViewModel to its entry, add the ViewModel store decorator to NavDisplay. Without it, koinViewModel() uses the store of the Activity, and every entry of the same route shares one instance:
NavDisplay(
backStack = backStack,
onBack = { backStack.removeLastOrNull() },
entryDecorators = listOf(
rememberSaveableStateHolderNavEntryDecorator(),
rememberViewModelStoreNavEntryDecorator()
),
entryProvider = koinEntryProvider()
)
Pass the route, or a field of it, as a parameter:
- Compiler Plugin DSL
- Annotations
- Classic DSL
class DetailViewModel(
@InjectedParam val route: DetailRoute,
private val repository: ItemRepository
) : ViewModel()
val detailModule = module {
viewModel<DetailViewModel>()
navigation<DetailRoute> { route ->
DetailScreen(viewModel = koinViewModel { parametersOf(route) })
}
}
@KoinViewModel
class DetailViewModel(
@InjectedParam val route: DetailRoute,
private val repository: ItemRepository
) : ViewModel()
// navigation<T> entries are declared in a DSL module
val detailModule = module {
navigation<DetailRoute> { route ->
DetailScreen(viewModel = koinViewModel { parametersOf(route) })
}
}
class DetailViewModel(
val route: DetailRoute,
private val repository: ItemRepository
) : ViewModel()
val detailModule = module {
viewModelOf(::DetailViewModel) // route comes from parametersOf(route)
navigation<DetailRoute> { route ->
DetailScreen(viewModel = koinViewModel { parametersOf(route) })
}
}
Entry metadata
navigation<T> takes a metadata map, passed to the NavEntry. Use it for per-entry transitions or scene strategies such as list-detail:
val conversationModule = module {
navigation<ConversationList>(metadata = ListDetailSceneStrategy.listPane()) {
ConversationListScreen()
}
navigation<ConversationDetail>(metadata = ListDetailSceneStrategy.detailPane()) { route ->
ConversationDetailScreen(route.id)
}
navigation<ModalRoute>(
metadata = NavDisplay.transitionSpec {
slideInVertically(initialOffsetY = { it }) togetherWith ExitTransition.KeepUntilTransitionsFinished
}
) {
ModalScreen()
}
}
Then pass the scene strategy to NavDisplay (sceneStrategy = rememberListDetailSceneStrategy()). The metadata keys come from Navigation 3 and the adaptive libraries, not from Koin.
From Navigation 2
| Navigation 2 | Navigation 3 with Koin |
|---|---|
String or typed route in NavHost { composable(...) } | @Serializable route class |
composable<Route> { ... } in the NavHost | navigation<Route> { ... } in a Koin module |
NavHost(navController, startDestination) | NavDisplay(backStack, entryProvider = koinEntryProvider()) |
Arguments read from SavedStateHandle | Route object passed to the lambda |
API summary
| API | Package | Purpose |
|---|---|---|
Module.navigation<T>(metadata) { } | org.koin.dsl.navigation3 | Declare an entry in a module |
ScopeDSL.navigation<T>(metadata) { } | org.koin.dsl.navigation3 | Declare an entry in a scope |
koinEntryProvider<T>(scope) | org.koin.compose.navigation3 | Entry provider from the current Compose scope |
entryProvider<T>(), getEntryProvider<T>() | org.koin.androidx.compose.navigation3 | Entry provider from an Activity or Fragment (Android) |
Related
- ViewModel in Compose:
koinViewModel()and its owners. - Android Scopes:
activityRetainedScopeand Activity scopes. - Injected Parameters:
parametersOfand@InjectedParam. - Koin for Compose: packages and starting Koin.
- Navigation 3 recipes: Google's samples for Navigation 3 itself.