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 of navigate("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+

Get New Tutorials by Email

No spam. Just clear, practical breakdowns you can apply right away.

Enjoy this tutorial?

Get new practical tech tutorials in your inbox.