Android / KMP Navigation
Principles
- Type-safe navigation with
@Serializableroute objects (KotlinX Serialization). - One nav graph per feature, defined in the feature's
presentationmodule. - Feature nav graphs are assembled in
:app. - Navigation within a feature uses a
NavControllerpassed into the feature nav graph. - Feature-to-feature navigation uses callbacks, keeping features decoupled.
Route Objects
Define routes as @Serializable objects or data classes in the feature's presentation module:
// feature:notes:presentation
@Serializable object NoteListRoute
@Serializable data class NoteDetailRoute(val noteId: String)
Use data object for screens with no parameters, data class for screens with arguments.
Feature Nav Graph
Each feature exposes a NavGraphBuilder extension function:
// feature:notes:presentation
fun NavGraphBuilder.notesGraph(
navController: NavController,
onNavigateToEditor: (String) -> Unit // callback for cross-feature navigation
) {
navigation<NoteListRoute>(startDestination = NoteListRoute) {
composable<NoteListRoute> {
NoteListRoot(
onNavigateToDetail = { navController.navigate(NoteDetailRoute(it)) }
)
}
composable<NoteDetailRoute> { backStackEntry ->
val route: NoteDetailRoute = backStackEntry.toRoute()
NoteDetailRoot(
noteId = route.noteId,
onNavigateToEditor = onNavigateToEditor
)
}
}
}
Wiring in :app
All feature nav graphs are assembled in one place:
// :app
NavHost(navController, startDestination = NoteListRoute) {
notesGraph(
navController = navController,
onNavigateToEditor = { navController.navigate(EditorRoute(it)) }
)
editorGraph(navController)
}
Cross-feature navigation is always expressed as a lambda callback — never by importing a route from another feature module.
Passing Arguments
For simple scalar arguments, use @Serializable data class routes:
@Serializable data class NoteDetailRoute(val noteId: String)
// Navigate
navController.navigate(NoteDetailRoute(noteId = "abc123"))
// Receive
composable<NoteDetailRoute> { backStackEntry ->
val route: NoteDetailRoute = backStackEntry.toRoute()
// route.noteId available here
}
Avoid passing complex objects via navigation — pass IDs and load data in the destination ViewModel.
Naming Conventions
| Thing | Convention | Example |
|---|---|---|
| Nav route | <Screen>Route |
NoteListRoute, NoteDetailRoute |
| Feature nav graph | <feature>Graph(...) on NavGraphBuilder |
notesGraph(...) |
Checklist: Adding Navigation to a New Feature
- Define
@Serializableroute objects for each screen infeature:presentation - Add feature nav graph function (
NavGraphBuilder.<feature>Graph(...)) - Pass
NavControllerfor intra-feature navigation - Expose cross-feature destinations as lambda callbacks (not direct route imports)
- Wire nav graph and cross-feature callbacks in
:app'sNavHost
