Skip to content

Instantly share code, notes, and snippets.

View AndroidPoet's full-sized avatar
God Mode

Android Poet AndroidPoet

God Mode
View GitHub Profile

The Complete Guide to Compose Multiplatform Navigation 3: From Theory to Production

Navigation is deceptively complex. At first glance, it seems simple: user taps a button, a new screen appears. But in reality, navigation is the thread that weaves together your entire application's state, lifecycle, animations, and user expectations. Get it wrong, and your app feels janky. Get it right, and users don't even think about it—which is exactly what you want.

For years, developers building cross-platform apps faced an impossible choice: build navigation separately for each platform (Android, iOS, Web, Desktop), duplicating logic and creating maintenance nightmares, or force a lowest-common-denominator approach that feels alien on each platform. Navigation 3 changes this completely.

This article is a deep dive into Compose Multiplatform Navigation 3, exploring the philosophy, architecture, and practical patterns that the nav3-recipes project demonstrates. Whether you're building a small side project or a la

@Composable
fun DetailScreen(
id: String,
navController: NavController
) {
var data by remember(id) { mutableStateOf<Data?>(null) }
LaunchedEffect(id) {
data = loadData(id)
}
NavHost(navController, Route.Home) {
composable<Route.Home> { HomeScreen() }
composable<Route.Profile> { ProfileScreen() }
navigation<Route.AdminSection, AdminRoute>(
startDestination = AdminRoute.Dashboard
) {
composable<AdminRoute.Dashboard> { AdminDashboard() }
composable<AdminRoute.Settings> { AdminSettings() }
}
@Composable
fun AppContent() {
val navController = rememberNavController()
BackHandler(enabled = navController.previousBackStackEntry != null) {
navController.popBackStack()
}
NavHost(navController, Route.Home) {
// Navigation routes
fun parseDeepLink(link: String): Route? {
return try {
when {
link.contains("/article/") -> Route.Article(link.substringAfterLast("/"))
else -> null
}
} catch (e: Exception) {
Log.e("DeepLink", "Failed to parse", e)
null
}
Button(onClick = { onDetailClick() })
Button(onClick = rememberCallback { navController.navigate(Route.Detail) })
@Composable
fun DetailScreen(navController: NavController) {
val viewModel = hiltViewModel<DetailViewModel>()
}
@Composable
fun DesktopLayout(navController: NavController) {
Row(modifier = Modifier.fillMaxSize()) {
Column(
modifier = Modifier
.width(250.dp)
.fillMaxHeight()
.background(Color.LightGray)
.padding(16.dp)
) {
@Composable
fun SyncWithBrowserHistory(navController: NavController) {
val currentRoute = navController.currentBackStackEntryAsState().value?.toRoute()
LaunchedEffect(currentRoute) {
val url = routeToUrl(currentRoute)
window.history.pushState(null, "", url)
}
LaunchedEffect(Unit) {
@Composable
fun AndroidBackHandler(navController: NavController) {
BackHandler {
if (!navController.popBackStack()) {
// Can't go back further, exit the app
}
}
}