What You Will Build
A multi-screen app with Navigation Compose where each screen is accessible via a route string (like profile/user123 or detail/42). The home screen lists clickable route cards, and each route navigates to a detail screen that extracts parameters from the URL-style path.
Why This Pattern Matters
Deep linking is essential for push notifications, web-to-app linking, and share URLs. Navigation Compose handles route parsing, argument extraction, and back stack management. Understanding this pattern is required for any multi-screen Compose app.
Step 1: Define the NavHost with Routes
@Composable
fun DeepLinkScreen() {
val navController = rememberNavController()
NavHost(
navController = navController,
startDestination = "home"
) {
composable("home") {
HomeScreen(navController)
}
composable("profile/{userId}") { backStackEntry ->
val userId = backStackEntry.arguments
?.getString("userId") ?: "unknown"
DetailScreen(
title = "Profile",
subtitle = "User: $userId",
icon = Icons.Default.Person,
color = Color(0xFF4A90D9)
) { navController.popBackStack() }
}
composable("settings") {
DetailScreen(
title = "Settings",
subtitle = "App Configuration",
icon = Icons.Default.Settings,
color = Color(0xFF50C878)
) { navController.popBackStack() }
}
composable("detail/{id}") { backStackEntry ->
val id = backStackEntry.arguments
?.getString("id") ?: "0"
DetailScreen(
title = "Detail",
subtitle = "Item #$id",
icon = Icons.Default.Article,
color = Color(0xFF9B59B6)
) { navController.popBackStack() }
}
}
}
Step 2: Route Cards on the Home Screen
val routes = listOf(
Triple("Profile Screen", "profile/user123",
Icons.Default.Person),
Triple("Settings Screen", "settings",
Icons.Default.Settings),
Triple("Item Detail", "detail/42",
Icons.Default.Article),
Triple("Search", "search",
Icons.Default.Search)
)
LazyColumn(contentPadding = PaddingValues(16.dp)) {
items(routes.size) { i ->
val (title, route, icon) = routes[i]
Card(
onClick = { navController.navigate(route) },
shape = RoundedCornerShape(12.dp)
) {
Row(modifier = Modifier.fillMaxWidth()
.padding(16.dp)) {
Icon(icon, null, tint = Color(0xFF4A90D9))
Spacer(Modifier.width(12.dp))
Column(Modifier.weight(1f)) {
Text(title,
fontWeight = FontWeight.SemiBold)
Text("/$route",
fontSize = 12.sp,
color = Color.Gray)
}
Icon(Icons.Default.ChevronRight, null)
}
}
}
}
Step 3: Reusable Detail Screen
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun DetailScreen(
title: String,
subtitle: String,
icon: ImageVector,
color: Color,
onBack: () -> Unit
) {
Scaffold(
topBar = {
TopAppBar(
title = { Text(title) },
navigationIcon = {
IconButton(onClick = onBack) {
Icon(Icons.Default.ArrowBack, "Back")
}
}
)
}
) { padding ->
Column(
modifier = Modifier.fillMaxSize()
.padding(padding),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Icon(icon, null,
modifier = Modifier.size(80.dp),
tint = color)
Text(title, fontSize = 28.sp,
fontWeight = FontWeight.Bold)
Text(subtitle, color = Color.Gray)
}
}
}
Tips and Pitfalls
- Argument types: By default, route arguments are strings. Use
navArgument("id") { type = NavType.IntType }for type-safe integer arguments. - Deep link from outside the app: Add
deepLinks = listOf(navDeepLink { uriPattern = "app://detail/{id}" })to the composable definition. - Back stack awareness: Use
popBackStack()instead ofnavigate("home")to avoid stacking duplicate home screens. - Add navigation-compose dependency:
implementation("androidx.navigation:navigation-compose:2.7.0")
Min SDK: API 21+ with Navigation Compose 2.5+