iwantcoding.com
🔥 Daily 👥 Rooms 🏆 Top Log in Sign up

Compose Navigation

Jetpack Compose Navigation gives you a typed graph of destinations, a back stack, deep links, and per-route ViewModels. The modern path (compose-navigation 2.8+) supports type-safe routes via @Serializable data classes, which removes the string-typed argument plumbing of older versions.

NavHost, typed routes, deep links, nested graphs

EXAMPLE
// build.gradle.kts
// implementation("androidx.navigation:navigation-compose:2.8.0")
// implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.3")

import androidx.compose.foundation.layout.*
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.navigation.NavType
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
import androidx.navigation.toRoute
import kotlinx.serialization.Serializable

// 1) Typed destinations — describe routes as data classes
@Serializable object Catalog
@Serializable data class ProductDetail(val sku: String)
@Serializable data class Checkout(val orderId: String)

@Composable
fun ShopNav() {
  val nav = rememberNavController()

  NavHost(navController = nav, startDestination = Catalog) {
    composable<Catalog> {
      CatalogScreen(onProductClick = { sku ->
        nav.navigate(ProductDetail(sku))
      })
    }

    composable<ProductDetail> { entry ->
      val args = entry.toRoute<ProductDetail>()
      ProductDetailScreen(
        sku = args.sku,
        onBuy = { orderId -> nav.navigate(Checkout(orderId)) },
        onBack = { nav.popBackStack() },
      )
    }

    composable<Checkout>(
      // 2) Deep link — open from a URL like shop://order/42
      deepLinks = listOf(navDeepLink { uriPattern = "shop://order/{orderId}" })
    ) { entry ->
      val args = entry.toRoute<Checkout>()
      CheckoutScreen(args.orderId, onDone = {
        nav.popBackStack(Catalog, inclusive = false)
      })
    }
  }
}

// 3) Per-destination ViewModel scoped to the back-stack entry
@Composable
fun CheckoutScreen(orderId: String, onDone: () -> Unit) {
  val vm: CheckoutViewModel = androidx.lifecycle.viewmodel.compose.viewModel()
  val ui by vm.ui.collectAsState()
  // .. UI uses ui state
}

class CheckoutViewModel : androidx.lifecycle.ViewModel() {
  // tied to the entry's lifecycle; cleared when navigating away
  val ui: kotlinx.coroutines.flow.StateFlow<Unit> = kotlinx.coroutines.flow.MutableStateFlow(Unit)
}

// 4) Nested graphs — group flows together
@Composable
fun RootNav() {
  val nav = rememberNavController()
  NavHost(nav, startDestination = "main") {
    navigation(startDestination = Catalog, route = "main") {
      composable<Catalog>       { CatalogScreen() }
      composable<ProductDetail> { ProductDetailScreen() }
    }
    navigation(startDestination = "login", route = "auth") {
      composable("login")  { LoginScreen() }
      composable("signup") { SignupScreen() }
    }
  }
}

// 5) Pass results back without holding refs
// In Checkout:
// nav.previousBackStackEntry?.savedStateHandle?.set("orderResult", "ok")
// nav.popBackStack()
// In Catalog (after returning):
// val handle = nav.currentBackStackEntry?.savedStateHandle
// val result = handle?.getStateFlow<String?>("orderResult", null)?.collectAsState()
// (then read result.value)

// 6) Modal / dialog destinations
// composable<EditProfile>(arguments = ..., enterTransition = { ... }) { ... }
// or use NavDisplay (3rd party) for declarative side-sheets.

// 7) Backstack management
// nav.popBackStack(Catalog, inclusive = false)
// nav.navigate(Catalog) { popUpTo(Catalog) { inclusive = true } } // reset stack

// 8) Common pitfalls
// - Holding context-bound singletons in a ViewModel that lives across config changes
// - String-typed routes (older compose-nav) -> typos = runtime crashes; prefer typed routes
// - Long argument blobs in routes; use the SavedStateHandle or a per-flow VM repository

// Stubs to keep this snippet compileable
@Composable fun CatalogScreen(onProductClick: (String) -> Unit = {}) {}
@Composable fun ProductDetailScreen(sku: String = "", onBuy: (String) -> Unit = {}, onBack: () -> Unit = {}) {}
@Composable fun ProductDetailScreen() {}
@Composable fun LoginScreen() {}
@Composable fun SignupScreen() {}

Why it matters

Use the new typed routes (@Serializable data classes + composable { entry.toRoute() }) and skip the string-typed argument plumbing of older versions. The compiler proves you pass every argument and a typo is a build error instead of a runtime crash on screen open.

Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.

Example

Example
val nav = rememberNavController()
NavHost(nav, startDestination = "home") {
    composable("home") { HomeScreen(nav) }
    composable("profile/{id}") { back -> ProfileScreen(back.arguments?.getString("id")) }
}
Try it Yourself »

Discussion

Loading…