100% Free Forever
AI-Powered Learning
Industry Expert Content
Certificates & Badges
Learn At Your Own Pace
Kotlin

Navigation Compose Basics

An introduction to the Navigation component's Compose integration — NavController, NavHost, and route-based destinations for moving between screens.

NavigationBeginner9 min readJul 8, 2026
Analogies

Before Jetpack Compose, Android navigation typically meant swapping Fragments inside an Activity, wired together with an XML navigation graph. Navigation Compose replaces that XML graph with a Kotlin DSL built around three cooperating pieces: a NavController that holds the navigation state and back stack, a NavHost composable that displays whichever destination is currently active, and a set of string-based 'routes' that identify each destination. The whole system is declarative — you describe which composable corresponds to which route, and the NavController handles pushing and popping the back stack as the user navigates.

🏏

Cricket analogy: Old-style Android navigation was like swapping players via a printed team sheet handed to the umpire before the match; Navigation Compose replaces it with a live digital lineup system where a NavController tracks who's on the field and a NavHost shows the current fielding arrangement by route.

A NavController is the single source of truth for 'where am I in the app.' It exposes functions like navigate(route) to push a new destination and popBackStack() to go back, and it tracks the full back stack of destinations the user has visited, similar to how the Activity back stack worked with Fragments, but scoped entirely within Compose without needing separate Fragment instances.

🏏

Cricket analogy: NavController is like the third umpire's official record of every decision and over bowled so far — it exposes actions like 'review this call' (navigate) and 'revert the previous decision' (popBackStack), always knowing exactly where the match stands.

Setting up NavHost

NavHost is the composable that actually renders the current destination. It takes the NavController, a startDestination route string, and a builder lambda where each destination is registered with composable(route) { ... }. Only the composable matching the current back stack entry is placed in the composition; when you navigate, Compose swaps it out and can animate the transition.

🏏

Cricket analogy: NavHost is like the stadium's big screen that only ever shows the currently active play — registered 'routes' like the batting view, bowling view, and scorecard view are all defined upfront, but only the one matching the current game state is displayed, with smooth transitions.

kotlin
@Composable
fun AppNavHost(navController: NavHostController = rememberNavController()) {
    NavHost(
        navController = navController,
        startDestination = "home"
    ) {
        composable("home") {
            HomeScreen(
                onOpenProfile = { navController.navigate("profile") }
            )
        }
        composable("profile") {
            ProfileScreen(
                onBack = { navController.popBackStack() }
            )
        }
        composable("settings") {
            SettingsScreen()
        }
    }
}

@Composable
fun HomeScreen(onOpenProfile: () -> Unit) {
    Column(modifier = Modifier.fillMaxSize().padding(24.dp)) {
        Text("Home", style = MaterialTheme.typography.headlineMedium)
        Spacer(Modifier.height(16.dp))
        Button(onClick = onOpenProfile) {
            Text("Go to Profile")
        }
    }
}

Where NavController lives

rememberNavController() creates a NavController scoped to the composition, surviving recompositions but not process death by itself (the Navigation library restores back stack state via rememberSaveable-backed mechanisms across configuration changes automatically). Only one NavController should typically own the top-level app navigation; it's usually created once near the root composable (often in the Activity's setContent block or a top-level App() composable) and passed down, or accessed via LocalNavController-style composition locals in larger apps.

🏏

Cricket analogy: rememberNavController() is like a scorer's live scorebook that survives every over (recomposition) but would be lost if the stadium's power failed entirely (process death) unless the scores are separately saved — typically one master scorebook is kept at the ground level and referenced by all commentary boxes.

Think of the NavController as a stack of index cards, one per screen visited, and NavHost as a single picture frame that always displays whatever card is on top. navigate() adds a card on top; popBackStack() removes the top card and reveals the one beneath it.

Calling navController.navigate(route) repeatedly on rapid taps (e.g. a double-tap on a button) can push duplicate destinations onto the back stack. Guard against this with launchSingleTop = true in navigate()'s NavOptions, or by debouncing the click handler.

Type-safe routes

Modern Navigation Compose (2.8+) supports type-safe, @Serializable route objects instead of raw strings, eliminating a class of typo-based bugs where a route string in navigate() doesn't exactly match the one registered in composable(). Even when using plain strings, it's good practice to centralize route constants in a single sealed class or object so route names aren't duplicated across the codebase.

🏏

Cricket analogy: Type-safe routes are like replacing hand-written scorecards, where a scorer might misspell 'LBW' inconsistently across sheets, with a standardized digital form that can't be filled in wrong — eliminating the class of typo bugs that plain route strings allow.

  • NavController holds the navigation back stack and exposes navigate()/popBackStack() to move between destinations.
  • NavHost is the composable container that displays whichever destination is currently on top of the back stack.
  • Each destination is registered with composable(route) { ... } inside the NavHost builder lambda.
  • rememberNavController() creates a composition-scoped controller that should generally live once near the root of the app.
  • Use launchSingleTop or debouncing to prevent duplicate destinations from rapid repeated navigate() calls.
  • Prefer centralized route definitions (or type-safe @Serializable routes) over scattered raw strings.

Practice what you learned

Was this page helpful?

Topics covered

#Kotlin#AndroidWithJetpackComposeStudyNotes#MobileDevelopment#NavigationComposeBasics#Navigation#Compose#Setting#NavHost#StudyNotes#SkillVeris