navigation-compose
Jetpack Compose Navigation Patterns
Dependencies
dependencies {
implementation("androidx.navigation:navigation-compose:2.8.5")
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.3")
}
Type-Safe Routes with Sealed Interface
@Serializable
sealed interface Route {
@Serializable
data object Home : Route
@Serializable
data object Settings : Route
@Serializable
data class UserProfile(val userId: String) : Route
@Serializable
data class PostDetail(val postId: Long, val showComments: Boolean = false) : Route
}
// For nested graphs
@Serializable
sealed interface AuthGraph {
@Serializable
data object Login : AuthGraph
@Serializable
data object Register : AuthGraph
@Serializable
data object ForgotPassword : AuthGraph
}
NavHost Configuration
@Composable
fun AppNavHost(
navController: NavHostController = rememberNavController(),
modifier: Modifier = Modifier
) {
NavHost(
navController = navController,
startDestination = Route.Home,
modifier = modifier
) {
composable<Route.Home> {
HomeScreen(
onNavigateToProfile = { userId ->
navController.navigate(Route.UserProfile(userId))
},
onNavigateToSettings = {
navController.navigate(Route.Settings)
}
)
}
composable<Route.Settings> {
SettingsScreen(onBack = { navController.popBackStack() })
}
composable<Route.UserProfile> { backStackEntry ->
val route = backStackEntry.toRoute<Route.UserProfile>()
UserProfileScreen(userId = route.userId)
}
composable<Route.PostDetail> { backStackEntry ->
val route = backStackEntry.toRoute<Route.PostDetail>()
PostDetailScreen(
postId = route.postId,
showComments = route.showComments
)
}
}
}
Argument Passing with Legacy navArgument
For non-serializable routes, use the classic approach:
composable(
route = "post/{postId}?showComments={showComments}",
arguments = listOf(
navArgument("postId") { type = NavType.LongType },
navArgument("showComments") {
type = NavType.BoolType
defaultValue = false
}
)
) { backStackEntry ->
val postId = backStackEntry.arguments?.getLong("postId") ?: return@composable
val showComments = backStackEntry.arguments?.getBoolean("showComments") ?: false
PostDetailScreen(postId = postId, showComments = showComments)
}
// Navigate
navController.navigate("post/$postId?showComments=true")
Navigation with Results (SavedStateHandle)
// Screen A: Navigate and listen for result
@Composable
fun ScreenA(navController: NavHostController) {
val result = navController.currentBackStackEntry
?.savedStateHandle
?.getStateFlow<String?>("selected_item", null)
?.collectAsState()
LaunchedEffect(result?.value) {
result?.value?.let { item ->
// Handle the result
}
}
Button(onClick = { navController.navigate(Route.ItemPicker) }) {
Text("Pick Item")
}
}
// Screen B: Set result and go back
@Composable
fun ItemPickerScreen(navController: NavHostController) {
Button(onClick = {
navController.previousBackStackEntry
?.savedStateHandle
?.set("selected_item", "chosen_value")
navController.popBackStack()
}) {
Text("Select This")
}
}
Deep Link Registration
composable<Route.PostDetail>(
deepLinks = listOf(
navDeepLink {
uriPattern = "https://example.com/posts/{postId}"
},
navDeepLink {
uriPattern = "myapp://posts/{postId}"
}
)
) { backStackEntry ->
val route = backStackEntry.toRoute<Route.PostDetail>()
PostDetailScreen(postId = route.postId)
}
AndroidManifest.xml intent filter:
<activity android:name=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="https" android:host="example.com" />
<data android:scheme="myapp" />
</intent-filter>
</activity>
Nested Navigation Graphs
fun NavGraphBuilder.authNavGraph(navController: NavHostController) {
navigation<AuthGraph.Login>(startDestination = AuthGraph.Login) {
composable<AuthGraph.Login> {
LoginScreen(
onLoginSuccess = {
navController.navigate(Route.Home) {
popUpTo(AuthGraph.Login) { inclusive = true }
}
},
onNavigateToRegister = {
navController.navigate(AuthGraph.Register)
}
)
}
composable<AuthGraph.Register> {
RegisterScreen(onBack = { navController.popBackStack() })
}
composable<AuthGraph.ForgotPassword> {
ForgotPasswordScreen(onBack = { navController.popBackStack() })
}
}
}
// In the main NavHost
NavHost(navController = navController, startDestination = AuthGraph.Login) {
authNavGraph(navController)
composable<Route.Home> { HomeScreen() }
}
Bottom Navigation
@Serializable
sealed interface BottomTab {
@Serializable data object Feed : BottomTab
@Serializable data object Search : BottomTab
@Serializable data object Profile : BottomTab
}
data class BottomNavItem(
val route: BottomTab,
val label: String,
val icon: ImageVector
)
val bottomNavItems = listOf(
BottomNavItem(BottomTab.Feed, "Feed", Icons.Default.Home),
BottomNavItem(BottomTab.Search, "Search", Icons.Default.Search),
BottomNavItem(BottomTab.Profile, "Profile", Icons.Default.Person)
)
@Composable
fun MainScreen() {
val navController = rememberNavController()
val navBackStackEntry by navController.currentBackStackEntryAsState()
Scaffold(
bottomBar = {
NavigationBar {
bottomNavItems.forEach { item ->
val isSelected = navBackStackEntry?.destination?.hasRoute(
item.route::class
) == true
NavigationBarItem(
selected = isSelected,
onClick = {
navController.navigate(item.route) {
popUpTo(navController.graph.findStartDestination().id) {
saveState = true
}
launchSingleTop = true
restoreState = true
}
},
icon = { Icon(item.icon, contentDescription = item.label) },
label = { Text(item.label) }
)
}
}
}
) { padding ->
NavHost(
navController = navController,
startDestination = BottomTab.Feed,
modifier = Modifier.padding(padding)
) {
composable<BottomTab.Feed> { FeedScreen() }
composable<BottomTab.Search> { SearchScreen() }
composable<BottomTab.Profile> { ProfileScreen() }
}
}
}
Animated Transitions
composable<Route.PostDetail>(
enterTransition = {
slideIntoContainer(
towards = AnimatedContentTransitionScope.SlideDirection.Left,
animationSpec = tween(300)
)
},
exitTransition = {
slideOutOfContainer(
towards = AnimatedContentTransitionScope.SlideDirection.Left,
animationSpec = tween(300)
)
},
popEnterTransition = {
slideIntoContainer(
towards = AnimatedContentTransitionScope.SlideDirection.Right,
animationSpec = tween(300)
)
},
popExitTransition = {
slideOutOfContainer(
towards = AnimatedContentTransitionScope.SlideDirection.Right,
animationSpec = tween(300)
)
}
) { backStackEntry ->
val route = backStackEntry.toRoute<Route.PostDetail>()
PostDetailScreen(postId = route.postId)
}
Navigation Testing
@Test
fun navigateToProfile_displaysUserProfile() {
val navController = TestNavHostController(ApplicationProvider.getApplicationContext())
composeTestRule.setContent {
navController.navigatorProvider.addNavigator(ComposeNavigator())
AppNavHost(navController = navController)
}
composeTestRule.onNodeWithText("View Profile").performClick()
val currentRoute = navController.currentBackStackEntry?.destination?.route
assertTrue(currentRoute?.contains("UserProfile") == true)
}
Best Practices
- Use type-safe routes with
@Serializabledata classes/objects over raw string routes. - Keep navigation logic out of composables; pass lambda callbacks (
onNavigateTo) instead. - Use
popUpTowithinclusive = truewhen navigating after login to clear the auth stack. - Use
launchSingleTop = truefor bottom tabs to prevent duplicate destinations. - Save and restore tab state with
saveState = trueandrestoreState = true. - Scope ViewModels to navigation entries with
hiltViewModel()orkoinViewModel(). - Test navigation by asserting on
navController.currentBackStackEntry.
More from ahmed3elshaer/everything-claude-code-mobile
koin-patterns
Koin dependency injection patterns for Android with modules, scopes, and ViewModel injection.
17kmp-repositories
Repository pattern for Kotlin Multiplatform. Shared interfaces with platform-specific implementations, clean data layer architecture.
15mobile-testing
Android testing patterns with JUnit5, Mockk, Turbine, and Compose testing for unit, integration, and UI tests.
9kmp-navigation
Navigation libraries for KMP. Voyager, Decompose, and platform-specific navigation (Compose Navigation, SwiftUI).
9jetpack-compose
Jetpack Compose patterns for declarative UI, state management, theming, animations, and performance optimization.
9expect-actual
Kotlin Multiplatform expect/actual patterns for platform-specific APIs. Learn to declare shared interfaces with platform implementations.
9