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.

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 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:
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:
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<T> block handles a specific destination type:
@Composable
fun AppNavigation(
navController: NavHostController = rememberNavController()
) {
NavHost(
navController = navController,
startDestination = Home
) {
composable<Home> {
HomeScreen(
onNavigateToProducts = { navController.navigate(ProductList) }
)
}
composable<ProductList> {
ProductListScreen(
onProductClick = { productId ->
navController.navigate(ProductDetail(productId))
}
)
}
composable<ProductDetail> { backStackEntry ->
// Extract typed route from back stack entry
val route: ProductDetail = backStackEntry.toRoute()
ProductDetailScreen(
productId = route.productId,
onCheckout = { cartId ->
navController.navigate(Checkout(cartId))
}
)
}
composable<Checkout> { backStackEntry ->
val route: Checkout = backStackEntry.toRoute()
CheckoutScreen(
cartId = route.cartId,
promoCode = route.promoCode
)
}
}
}The toRoute<T>() 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:
@Serializable
object AuthGraph // Graph identifier
@Serializable
object Login
@Serializable
object Register
@Serializable
data class PasswordReset(val email: String)
fun NavGraphBuilder.authNavGraph(navController: NavHostController) {
navigation<AuthGraph>(startDestination = Login) {
composable<Login> {
LoginScreen(
onRegisterClick = { navController.navigate(Register) },
onForgotPassword = { email ->
navController.navigate(PasswordReset(email))
},
onLoginSuccess = {
navController.navigate(Home) {
popUpTo<AuthGraph> { inclusive = true }
}
}
)
}
composable<Register> {
RegisterScreen(
onRegistrationComplete = {
navController.navigate(Home) {
popUpTo<AuthGraph> { inclusive = true }
}
}
)
}
composable<PasswordReset> { backStackEntry ->
val route: PasswordReset = backStackEntry.toRoute()
PasswordResetScreen(email = route.email)
}
}
}The popUpTo<AuthGraph> with inclusive = true clears the entire auth flow from the back stack after successful login, preventing users from navigating back to login screens.
Ready to ace your Android interviews?
Practice with our interactive simulators, flashcards, and technical tests.
Value Classes as Route Arguments
Navigation 2.9.0-alpha03 added support for value classes in routes. This pattern enforces type safety for IDs and prevents mixing up parameters of the same primitive type:
@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:
composable<ProductDetail>(
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:
<!-- AndroidManifest.xml -->
<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"
android:pathPrefix="/products" />
</intent-filter>
</activity>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:
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?
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:
@HiltViewModel
class ProductViewModel @Inject constructor(
private val repository: ProductRepository,
savedStateHandle: SavedStateHandle
) : ViewModel() {
// Extract typed route from SavedStateHandle
private val productId: String = savedStateHandle.toRoute<ProductDetail>().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:
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<ProductDetail>()
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:
@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<Home> { 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.
Start practicing!
Test your knowledge with our interview simulators and technical tests.
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:
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<ProductDetail> { 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
@Serializabledata 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 covers these patterns in depth
- See also Jetpack Compose interview questions for related UI concepts
Can you spot the bug in Android?
One real snippet, one hidden bug, one attempt a day. No account needed to try.

Written by
Anthony Fillion-MailletFounder of SharpSkill
Full-stack developer for over 10 years. Runs SharpSkill and answers for everything published here.
Updated on September 1, 2026
Tags
Share
Related articles

Android Modularization in 2026: Multi-Module Architecture and Interview Questions
Master Android multi-module architecture with convention plugins, Gradle version catalogs, and feature modules. Includes common interview questions on modularization strategies.

Android WorkManager in 2026: Background Tasks, Constraints and Interview Questions
Master Android WorkManager for reliable background task execution. Learn constraints, chaining, periodic work, and common interview questions with practical Kotlin examples.

Kotlin Flow vs StateFlow vs SharedFlow: Android Interview Questions in 2026
The Kotlin Flow vs StateFlow vs SharedFlow questions Android interviewers ask in 2026, with clear answers, a comparison table, and production-ready code.