Skip to main content
Version: 4.2

Navigation 3

Koin provides integration with AndroidX Navigation 3 for type-safe, multiplatform navigation with dependency injection.

What is Navigation 3?

Navigation 3 is Jetpack's new navigation library designed specifically for Compose:

  • Full back stack control - Navigate by adding/removing items from a list
  • Type-safe routes - Routes are Kotlin classes with @Serializable
  • Adaptive layouts - Display multiple destinations simultaneously (list-detail)
  • Automatic animations - Built-in transition support

Setup

Multiplatform Projects

// shared/build.gradle.kts
commonMain.dependencies {
implementation("io.insert-koin:koin-compose-navigation3:$koin_version")
implementation("org.jetbrains.kotlinx:kotlinx-serialization-core:$serialization_version")
}

Android-only Projects

dependencies {
implementation("io.insert-koin:koin-compose-navigation3:$koin_version")
implementation("org.jetbrains.kotlinx:kotlinx-serialization-core:$serialization_version")
}

Apply the serialization plugin:

plugins {
kotlin("plugin.serialization")
}

Platform Support

PlatformStatus
AndroidFull support
iOSFull support
DesktopFull support
WebFull support

Core Concepts

Routes as Kotlin Classes

Define type-safe routes using @Serializable:

@Serializable
data object HomeRoute

@Serializable
data object ProfileRoute

@Serializable
data class DetailRoute(val itemId: String)

@Serializable
data class SettingsRoute(val section: String? = null)

Back Stack

Navigation 3 uses a simple list-based back stack:

// Basic back stack
val backStack = remember { mutableStateListOf<Any>(HomeRoute) }

// Persistent back stack (survives config changes)
val backStack = rememberNavBackStack(HomeRoute)

// Navigate forward
backStack.add(DetailRoute("123"))

// Navigate back
backStack.removeLastOrNull()

NavDisplay renders the back stack with animations:

NavDisplay(
backStack = backStack,
onBack = { backStack.removeLastOrNull() },
entryProvider = { route -> /* NavEntry */ }
)

Koin Integration

Declaring Navigation Entries

Use the navigation<T> DSL in your modules:

val appModule = module {
// Dependencies
single<ApiClient>()
viewModel<HomeViewModel>()
viewModel<DetailViewModel>()

// Navigation entries with Koin injection
navigation<HomeRoute> { route ->
HomeScreen(viewModel = koinViewModel())
}

navigation<DetailRoute> { route ->
DetailScreen(
itemId = route.itemId,
viewModel = koinViewModel { parametersOf(route.itemId) }
)
}

navigation<ProfileRoute> { route ->
ProfileScreen(viewModel = koinViewModel())
}
}

Using koinEntryProvider

Retrieve all navigation entries from Koin:

@Composable
fun App() {
val backStack = rememberNavBackStack(HomeRoute)
val entryProvider = koinEntryProvider<Any>()

NavDisplay(
backStack = backStack,
onBack = { backStack.removeLastOrNull() },
entryProvider = entryProvider
)
}

Complete Example

// Routes
@Serializable data object ConversationList
@Serializable data class ConversationDetail(val id: Int)
@Serializable data object Profile

// Navigator class for cleaner navigation
class Navigator(startDestination: Any) {
val backStack = mutableStateListOf(startDestination)

fun goTo(destination: Any) {
backStack.add(destination)
}

fun goBack() {
backStack.removeLastOrNull()
}
}

// Koin modules
val appModule = module {
includes(conversationModule, profileModule)

activityRetainedScope {
scoped { Navigator(startDestination = ConversationList) }
}
}

val conversationModule = module {
activityRetainedScope {
navigation<ConversationList> {
val navigator = get<Navigator>()
ConversationListScreen(
onConversationClicked = { detail ->
navigator.goTo(detail)
}
)
}

navigation<ConversationDetail> { route ->
val navigator = get<Navigator>()
ConversationDetailScreen(
conversationId = route.id,
onProfileClicked = { navigator.goTo(Profile) }
)
}
}
}

val profileModule = module {
activityRetainedScope {
navigation<Profile> {
ProfileScreen()
}
}
}

// Activity
class MainActivity : ComponentActivity(), AndroidScopeComponent {
override val scope: Scope by activityRetainedScope()

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

setContent {
val navigator: Navigator = get()

Scaffold { padding ->
NavDisplay(
backStack = navigator.backStack,
modifier = Modifier.padding(padding),
onBack = { navigator.goBack() },
entryProvider = getEntryProvider()
)
}
}
}
}

Scoped Navigation

Declare navigation entries within Koin scopes:

val appModule = module {
// Activity-retained scope (survives config changes)
activityRetainedScope {
scoped { UserSession() }
viewModel<ProfileViewModel>()

navigation<ProfileRoute> { route ->
ProfileScreen(viewModel = koinViewModel())
}
}

// Custom scope
scope<CheckoutFlow> {
scoped { CheckoutState() }
viewModel<CheckoutViewModel>()

navigation<CartRoute> { route ->
CartScreen(viewModel = koinViewModel())
}

navigation<PaymentRoute> { route ->
PaymentScreen(viewModel = koinViewModel())
}
}
}

ViewModel Integration

With Navigation Arguments

Pass route data to ViewModels:

@Serializable
data class DetailRoute(val itemId: String, val fromSearch: Boolean = false)

class DetailViewModel(
val route: DetailRoute,
private val repository: Repository
) : ViewModel() {
val item = repository.getItem(route.itemId)
}

val appModule = module {
viewModelOf(::DetailViewModel)

navigation<DetailRoute> { route ->
DetailScreen(
viewModel = koinViewModel { parametersOf(route) }
)
}
}

With Entry Decorators

Use decorators for ViewModel state retention:

NavDisplay(
backStack = backStack,
onBack = { backStack.removeLastOrNull() },
entryDecorators = listOf(
rememberSaveableStateHolderNavEntryDecorator(),
rememberViewModelStoreNavEntryDecorator()
),
entryProvider = entryProvider {
entry<DetailRoute> { route ->
val viewModel = koinViewModel<DetailViewModel> {
parametersOf(route)
}
DetailScreen(viewModel)
}
}
)

Animations

Default Transitions

NavDisplay(
backStack = backStack,
onBack = { backStack.removeLastOrNull() },
entryProvider = entryProvider,
// Forward navigation animation
transitionSpec = {
slideInHorizontally(initialOffsetX = { it }) togetherWith
slideOutHorizontally(targetOffsetX = { -it })
},
// Back navigation animation
popTransitionSpec = {
slideInHorizontally(initialOffsetX = { -it }) togetherWith
slideOutHorizontally(targetOffsetX = { it })
}
)

Per-Route Animations

navigation<ModalRoute>(
metadata = NavDisplay.transitionSpec {
slideInVertically(initialOffsetY = { it }) togetherWith
ExitTransition.KeepUntilTransitionsFinished
} + NavDisplay.popTransitionSpec {
EnterTransition.None togetherWith
slideOutVertically(targetOffsetY = { it })
}
) { route ->
ModalScreen()
}

Adaptive Layouts

List-Detail Pattern

Use scene strategies for adaptive layouts:

@Composable
fun App() {
val backStack = rememberNavBackStack(ConversationList)
val listDetailStrategy = rememberListDetailSceneStrategy<Any>()

NavDisplay(
backStack = backStack,
onBack = { backStack.removeLastOrNull() },
sceneStrategy = listDetailStrategy,
entryProvider = entryProvider {
entry<ConversationList>(
metadata = ListDetailSceneStrategy.listPane()
) {
ConversationListScreen()
}

entry<ConversationDetail>(
metadata = ListDetailSceneStrategy.detailPane()
) { route ->
ConversationDetailScreen(route.id)
}
}
)
}

With Koin Modules

val appModule = module {
navigation<ConversationList>(
metadata = ListDetailSceneStrategy.listPane()
) {
ConversationListScreen(
onItemClick = { get<Navigator>().goTo(it) }
)
}

navigation<ConversationDetail>(
metadata = ListDetailSceneStrategy.detailPane()
) { route ->
ConversationDetailScreen(route.id)
}
}

Android Extensions

Lazy Entry Provider

class MainActivity : ComponentActivity() {
// Lazy initialization
private val entryProvider by entryProvider<Any>()

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
val backStack = rememberNavBackStack(HomeRoute)

NavDisplay(
backStack = backStack,
onBack = { backStack.removeLastOrNull() },
entryProvider = entryProvider
)
}
}
}

Eager Entry Provider

class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

val entryProvider = getEntryProvider<Any>()

setContent {
NavDisplay(
backStack = backStack,
entryProvider = entryProvider,
onBack = { backStack.removeLastOrNull() }
)
}
}
}

API Reference

DSL Functions

FunctionDescription
Module.navigation<T> { }Declare navigation entry at module level
ScopeDSL.navigation<T> { }Declare navigation entry within a scope

Composable Functions

FunctionDescription
koinEntryProvider<T>()Get aggregated entry provider from Koin

Android Extensions

FunctionDescription
entryProvider<T>()Lazy entry provider delegate
getEntryProvider<T>()Eager entry provider

Migration from Navigation 2.x

Before (Navigation 2.x)

NavHost(navController, startDestination = "home") {
composable("home") {
HomeScreen(viewModel = koinViewModel())
}
composable("detail/{id}") { backStackEntry ->
val id = backStackEntry.arguments?.getString("id")
DetailScreen(id = id, viewModel = koinViewModel())
}
}

After (Navigation 3)

// Type-safe routes
@Serializable data object HomeRoute
@Serializable data class DetailRoute(val id: String)

// Module declaration
val appModule = module {
navigation<HomeRoute> { HomeScreen(viewModel = koinViewModel()) }
navigation<DetailRoute> { route ->
DetailScreen(id = route.id, viewModel = koinViewModel())
}
}

// Usage
val backStack = rememberNavBackStack(HomeRoute)
NavDisplay(
backStack = backStack,
onBack = { backStack.removeLastOrNull() },
entryProvider = koinEntryProvider()
)

Resources