DEV Community

Cover image for Jetpack Compose Navigation(Interview Prep)
Aalaa Fahiem
Aalaa Fahiem

Posted on

Jetpack Compose Navigation(Interview Prep)

About this article: This is based on hands-on review of a real project. The goal is understanding over memorization — because interviewers can always tell the difference.

  1. Screen.Details.route vs Screen.Details.createRoute(name) — What's the Difference? This trips up a lot of people because both look like they're "the route." They're not. Screen.Details.route is a template — it contains a placeholder: "details/{pokemonName}" You use this when registering the destination inside NavHost, so the navigation system knows the shape of the route and which argument to extract. Screen.Details.createRoute(name) produces a real URL like: "details/Pikachu" You use this when navigating — navController.navigate(...) needs an actual address with real data embedded.

⚠️ What breaks if you use .route in both places?
NavHost would still match the route, but the extracted argument would be the literal string "{pokemonName}" instead of the actual Pokémon name. No crash — just silent wrong data.

Key Rule → Template for registration. Real URL for navigation.

  1. Defining a Route in NavConstants Doesn't Register It If NavConstants.kt defines Search and Settings but they never appear in AppNavigation, what happens when you call navController.navigate(Screen.Search.route)? The app crashes at runtime: IllegalArgumentException: "navigation destination 'search' is unknown to this NavController" Here's why this confuses people:

NavConstants.kt is just a dictionary of strings — defining a route there costs nothing and guarantees nothing.
AppNavigation.kt is where destinations actually exist — a route only becomes real when it has a matching composable() registered inside NavHost.

NavConstants protects you from typos. It cannot protect you from forgetting to register the destination. Both mistakes crash the app.
Key Rule → You need both — the string AND the composable() registration.

  1. nullable = false and if (name != null) Are Not Contradicting Each Other
    You might see navArgument("pokemonName") { nullable = false } declared, and then right below it, the code still does ?. and if (name != null). That looks contradictory. It's not.
    They operate at completely different layers:
    nullable = falseif (name != null)LayerNavigation systemKotlin type systemWhen it actsRuntime navigationCompile timeWhat it doesCrashes if arg is missingSatisfies the compiler
    Bundle.getString() always returns String? regardless of your navigation contract — the Kotlin compiler has no idea about navArgument rules. The null check is there to satisfy the compiler, not because you genuinely expect null at runtime.
    Key Rule → Navigation contract and Kotlin types are separate systems that don't talk to each other.

  2. popBackStack() vs navigate(Home) — Never Substitute One for the Other
    Both can result in the user seeing the Home screen. But what they do to the back stack is completely different.
    popBackStack() removes the current screen and reveals what was beneath it:
    Before: [Home, Details]
    After: [Home] ← Details removed, Home revealed
    navigate(Screen.Home.route) pushes a new Home screen on top:
    Before: [Home, Details]
    After: [Home, Details, Home] ← new Home added

⚠️ Why it matters: After navigate(), pressing Back lands the user on Details — a screen they thought they left. Press again, they're on the original Home. The back stack grows silently and creates a broken experience.

Key Rule → Use popBackStack() to go back. Use navigate() to go forward. Never substitute.

  1. When Do You Actually Need backStackEntry?
    Inside NavHost, the lambda for composable() can optionally name its parameter backStackEntry. Some composables do, some don't. Why?
    backStackEntry gives access to arguments embedded in the route. Screens with static routes like "home" or "profile" have no arguments — there's nothing to extract, so backStackEntry is ignored and not named.
    Details has {pokemonName} in its route. When navController.navigate("details/Pikachu") was called, the navigation system stored "Pikachu" in the backStackEntry's argument bundle. backStackEntry.arguments?.getString("pokemonName") pulls it back out.
    Key Rule → You only need backStackEntry when the route carries arguments.

  2. The Three Layers of Compose Navigation
    Understanding these three layers separately makes everything else click.
    🔵 Navigation System Layer

Rules enforced at runtime by NavHost/NavController
Every route navigated to must be registered with composable() — otherwise crash
nullable = false is a contract here — violate it, it throws
Doesn't know Kotlin types

🟣 Kotlin Type System Layer

Rules enforced at compile time by the compiler
Bundle.getString() returns String? — compiler forces null handling
Has no awareness of navigation contracts or navArgument rules
Doesn't know your back stack state

🟢 Back Stack Layer

Runtime state — the history of where the user has been
navigate() always pushes a new destination
popBackStack() always removes the top destination
System Back button also pops it
Keeps growing until something pops it

A quick comparison between the two that are most often confused:
Kotlin Type SystemBack StackNatureCompile-time rulesRuntime stateCares aboutNullability, typesNavigation historyExamplegetString() returns String?[Home, Details] grows with navigate()

  1. Navigation Hoisting — Screens Get Lambdas, Not NavController Navigation hoisting means screen composables receive navigation actions as lambda parameters instead of having direct access to navController. kotlin// AppNavigation owns navController, passes actions as lambdas HomeScreen( onNavigateToProfile = { navController.navigate(Screen.Profile.route) }, onPokemonClick = { name -> navController.navigate(Screen.Details.createRoute(name)) } )

// HomeScreen knows nothing about navController
@Composable
fun HomeScreen(
onNavigateToProfile: () -> Unit,
onPokemonClick: (String) -> Unit
)
Why this pattern exists:

Separates UI logic from navigation logic
HomeScreen becomes testable without a real NavController
Screens stay reusable — they don't care how navigation is implemented

Notice the subtle difference between the two lambdas above:

onNavigateToProfile: () -> Unit — no data needed; Profile renders itself without input
onPokemonClick: (String) -> Unit — carries the Pokémon name; Details needs it to know what to show

Key Rule → Screens get lambdas, not navController.

Quick Reference Card
ConceptRule.route vs createRoute()Template to register, real URL to navigateUnregistered routeCrashes at runtime with IllegalArgumentExceptionnullable = falseNavigation contract — not Kotlin null safetypopBackStack() vs navigate()Go back vs go forward — never substitutebackStackEntryOnly needed when the route carries argumentsNavigation hoistingScreens get lambdas, not navControllerNavConstantsPrevents typos, not missing registrations

Found this useful? These notes came from reviewing a real Compose project end-to-end — the kind of hands-on work that makes interview answers actually stick.

Top comments (0)