Compose + NavHost + NavFlow
Full example from sample-app-compose: NavHost owns tabs/routes while Kmposable powers inner flows.
This mirrors the structure in sample-app-compose: NavHost owns tabs/routes while Kmposable handles
inner flows.
@Composable
fun App() {
val navController = rememberNavController()
val repository = remember { InMemoryContactsRepository(seedData) }
MaterialTheme {
Scaffold(bottomBar = { AppBottomBar(navController) }) { paddingValues ->
NavHost(
navController = navController,
startDestination = "contacts",
modifier = Modifier.padding(paddingValues)
) {
composable("contacts") {
ContactsDestination(repository, navController)
}
composable("settings") {
SettingsDestination()
}
}
}
}
}
Contacts destination
@Composable
fun ContactsDestination(
repository: InMemoryContactsRepository,
navController: NavHostController
) {
val navFlow = rememberNavFlow(key = repository) { scope ->
ContactsNavFlow(repository = repository, appScope = scope)
}
val renderer = remember {
nodeRenderer<ContactsFlowEvent> {
register<ContactsListNode> { node ->
val state by node.state.collectAsState()
ContactsListScreen(state = state, onEvent = node::onEvent)
}
register<ContactDetailsNode> { node ->
val state by node.state.collectAsState()
ContactDetailsScreen(state = state, onEvent = node::onEvent)
}
}
}
LaunchedEffect(navFlow) {
navFlow.outputs.collect { output ->
when (output) {
is ContactsFlowEvent.OpenContact ->
navController.navigate("contactDetails/${output.id}")
ContactsFlowEvent.Done -> navController.popBackStack()
}
}
}
NavFlowHost(navFlow = navFlow, renderer = renderer)
}
Why it matters
- Keeps NavHost responsible for top-level navigation (tabs, deep links).
- Kmposable stays reusable/headless inside each destination.
- Outputs map directly to NavController actions, no wrapper interfaces.