# Jetpack Navigation Compose in 2026: Type-Safe Navigation and Interview Questions > Master Jetpack Navigation Compose with type-safe routes using Kotlin Serialization. Learn Navigation 2.10, predictive back, argument passing, and common interview questions. - Published: 2026-09-01 - Updated: 2026-09-01 - Author: Anthony Fillion-Maillet - Tags: android, jetpack-compose, navigation, kotlin, type-safe - Reading time: 9 min --- Jetpack Navigation Compose 2.10 brings compile-time type safety to Android navigation through Kotlin Serialization. This approach eliminates runtime crashes from mismatched arguments and simplifies refactoring across large codebases. > **Navigation 2.10 Stable** > > Navigation Compose 2.10.0, released August 2026, requires minimum SDK 24. Type-safe routes use `@Serializable` data classes instead of string-based destinations, catching navigation errors at compile time. ## Setting Up Type-Safe Navigation in Navigation 2.10 The type-safe navigation API requires Kotlin Serialization. Add these dependencies to your module-level `build.gradle.kts`: ```kotlin // build.gradle.kts plugins { id("org.jetbrains.kotlin.plugin.serialization") version "2.1.0" } dependencies { implementation("androidx.navigation:navigation-compose:2.10.0") implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.3") } ``` Define destinations as serializable types. Use `object` for screens without arguments and `data class` for screens that receive parameters: ```kotlin // Routes.kt import kotlinx.serialization.Serializable @Serializable object Home @Serializable object ProductList @Serializable data class ProductDetail(val productId: String) @Serializable data class Checkout(val cartId: String, val promoCode: String? = null) ``` The compiler enforces that all route classes are annotated with `@Serializable`. Missing annotations trigger lint errors in Navigation 2.9+. ## Building a NavHost with Serializable Routes The `NavHost` composable accepts serializable types directly. Each `composable` block handles a specific destination type: ```kotlin // AppNavigation.kt @Composable fun AppNavigation( navController: NavHostController = rememberNavController() ) { NavHost( navController = navController, startDestination = Home ) { composable { HomeScreen( onNavigateToProducts = { navController.navigate(ProductList) } ) } composable { ProductListScreen( onProductClick = { productId -> navController.navigate(ProductDetail(productId)) } ) } composable { backStackEntry -> // Extract typed route from back stack entry val route: ProductDetail = backStackEntry.toRoute() ProductDetailScreen( productId = route.productId, onCheckout = { cartId -> navController.navigate(Checkout(cartId)) } ) } composable { backStackEntry -> val route: Checkout = backStackEntry.toRoute() CheckoutScreen( cartId = route.cartId, promoCode = route.promoCode ) } } } ``` The `toRoute()` extension returns a fully typed instance of the destination. Renaming a property in the data class updates all usages automatically through IDE refactoring. ## Nested Navigation Graphs for Feature Modules Large applications benefit from nested navigation graphs. Each feature module defines its own graph, and the root graph composes them: ```kotlin // AuthNavigation.kt @Serializable object AuthGraph // Graph identifier @Serializable object Login @Serializable object Register @Serializable data class PasswordReset(val email: String) fun NavGraphBuilder.authNavGraph(navController: NavHostController) { navigation(startDestination = Login) { composable { LoginScreen( onRegisterClick = { navController.navigate(Register) }, onForgotPassword = { email -> navController.navigate(PasswordReset(email)) }, onLoginSuccess = { navController.navigate(Home) { popUpTo { inclusive = true } } } ) } composable { RegisterScreen( onRegistrationComplete = { navController.navigate(Home) { popUpTo { inclusive = true } } } ) } composable { backStackEntry -> val route: PasswordReset = backStackEntry.toRoute() PasswordResetScreen(email = route.email) } } } ``` The `popUpTo` with `inclusive = true` clears the entire auth flow from the back stack after successful login, preventing users from navigating back to login screens. ## Value Classes as Route Arguments Navigation 2.9.0-alpha03 added support for [value classes](https://kotlinlang.org/docs/inline-classes.html) in routes. This pattern enforces type safety for IDs and prevents mixing up parameters of the same primitive type: ```kotlin // Domain.kt @JvmInline @Serializable value class ProductId(val value: String) @JvmInline @Serializable value class UserId(val value: String) @Serializable data class ProductDetail(val productId: ProductId) @Serializable data class UserProfile(val userId: UserId) ``` Now the compiler prevents passing a `UserId` where a `ProductId` is expected, a common bug in string-based navigation. ## Deep Links with Type-Safe Routes Deep links map external URIs to serializable routes. Define the pattern in the `composable` block: ```kotlin // AppNavigation.kt composable( deepLinks = listOf( navDeepLink { uriPattern = "https://example.com/products/{productId}" } ) ) { backStackEntry -> val route: ProductDetail = backStackEntry.toRoute() ProductDetailScreen(productId = route.productId) } ``` The `{productId}` placeholder maps directly to the `productId` property in the `ProductDetail` data class. Navigation extracts and deserializes the argument automatically. Register deep links in the `AndroidManifest.xml`: ```xml ``` ## Predictive Back Gesture with Navigation Compose Android 14+ supports predictive back gestures where users preview the previous screen before committing. Navigation Compose 2.10 integrates with this through `SeekableTransitionState`: ```kotlin // AppNavigation.kt NavHost( navController = navController, startDestination = Home, enterTransition = { slideIntoContainer(AnimatedContentTransitionScope.SlideDirection.Start) }, exitTransition = { slideOutOfContainer(AnimatedContentTransitionScope.SlideDirection.Start) }, popEnterTransition = { slideIntoContainer(AnimatedContentTransitionScope.SlideDirection.End) }, popExitTransition = { slideOutOfContainer(AnimatedContentTransitionScope.SlideDirection.End) } ) { // destinations } ``` The predictive back gesture shows the previous destination in real-time as the user swipes. Releasing the gesture before the threshold cancels navigation and returns to the current screen. ## Common Interview Questions on Navigation Compose Senior Android interviews often probe navigation architecture decisions. Here are questions that distinguish experienced candidates. ### Why use type-safe navigation over string-based routes? String-based routes like `"product/{productId}"` cause runtime crashes when arguments are misspelled or have wrong types. Type-safe routes catch these errors at compile time. Refactoring a route parameter updates all usages through IDE tooling, while string-based routes require manual find-and-replace across the codebase. ### How do you share data between destinations without passing large objects? > **Avoid Large Route Arguments** > > Routes serialize into saved instance state. Passing large objects like product catalogs risks `TransactionTooLargeException`. Pass an ID and load data from a repository or shared ViewModel instead. The pattern for sharing data involves scoped ViewModels: ```kotlin // ProductViewModel.kt @HiltViewModel class ProductViewModel @Inject constructor( private val repository: ProductRepository, savedStateHandle: SavedStateHandle ) : ViewModel() { // Extract typed route from SavedStateHandle private val productId: String = savedStateHandle.toRoute().productId val product = repository.getProduct(productId) .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), null) } ``` ### How do you test navigation in Compose? Navigation testing verifies that user actions trigger correct destination changes: ```kotlin // NavigationTest.kt class NavigationTest { @get:Rule val composeTestRule = createComposeRule() @Test fun clickingProduct_navigatesToDetail() { val navController = TestNavHostController(ApplicationProvider.getApplicationContext()) navController.navigatorProvider.addNavigator(ComposeNavigator()) composeTestRule.setContent { AppNavigation(navController = navController) } // Navigate to product list composeTestRule.onNodeWithText("View Products").performClick() // Click first product composeTestRule.onNodeWithTag("product_item_0").performClick() // Verify current destination val currentRoute = navController.currentBackStackEntry?.toRoute() assertThat(currentRoute).isNotNull() } } ``` ### What happens when a NavController is used after being destroyed? Navigation 2.9.0-alpha01 changed this behavior. Using a `NavController` after the hosting `Activity` or `Fragment` is destroyed now throws `IllegalStateException`. Previous versions silently failed or produced undefined behavior. This change surfaces lifecycle bugs earlier in development. ### How do you handle conditional navigation based on auth state? A common pattern observes auth state and redirects accordingly: ```kotlin // MainScreen.kt @Composable fun MainScreen( authState: AuthState, navController: NavHostController = rememberNavController() ) { LaunchedEffect(authState) { when (authState) { is AuthState.Authenticated -> { navController.navigate(Home) { popUpTo(0) { inclusive = true } } } is AuthState.Unauthenticated -> { navController.navigate(AuthGraph) { popUpTo(0) { inclusive = true } } } else -> { /* loading, do nothing */ } } } NavHost( navController = navController, startDestination = if (authState is AuthState.Authenticated) Home else AuthGraph ) { authNavGraph(navController) composable { HomeScreen() } } } ``` The `LaunchedEffect` reacts to auth state changes and clears the entire back stack with `popUpTo(0) { inclusive = true }` to prevent back navigation to the wrong flow. ## Migrating from String Routes to Type-Safe Navigation Existing projects can migrate incrementally. Both string routes and type-safe routes coexist in the same `NavHost`: ```kotlin // AppNavigation.kt NavHost( navController = navController, startDestination = "home" // Legacy string route ) { // Legacy string-based destination composable("home") { HomeScreen(onNavigateToProduct = { id -> navController.navigate(ProductDetail(id)) // New type-safe navigation }) } // New type-safe destination composable { backStackEntry -> val route: ProductDetail = backStackEntry.toRoute() ProductDetailScreen(productId = route.productId) } } ``` This approach allows migrating one screen at a time without rewriting the entire navigation graph. ## Key Takeaways for Jetpack Navigation Compose in 2026 - Navigation 2.10.0 is the current stable version with minimum SDK 24 - Type-safe routes use `@Serializable` data classes instead of string destinations - Value classes provide additional type safety for IDs and prevent parameter mix-ups - Nested navigation graphs organize feature modules with independent back stacks - Deep links map URI patterns directly to route properties - Predictive back gestures integrate automatically with standard transitions - Route arguments should stay lightweight: pass IDs, load data from repositories - Migration from string routes can happen incrementally with mixed navigation graphs - [Android interview preparation](/technologies/android) covers these patterns in depth - See also [Jetpack Compose interview questions](/blog/android/jetpack-compose-interview-questions) for related UI concepts --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/en/blog/android/jetpack-navigation-compose-type-safe-2026