NavController and NavHost

Two objects do the work. The NavController, created with rememberNavController(), owns the back stack: navigate(route) pushes a destination, popBackStack() pops one, and the system Back gesture pops automatically. It saves the stack with the activity's saved state, so the user returns to the same screen after rotation or process death. The NavHost composable takes the controller and a start destination, declares the graph in a builder block, and shows the composable of whatever destination is on top:

ui/nav/BookNestNav.kt: the graph (excerpt)Kotlin
      NavHost(nav, startDestination = ShopGraph, Modifier.padding(inner)) {
        navigation<ShopGraph>(startDestination = Catalog) {
          composable<Catalog> {
            CatalogList(books, this, onOpen = { book -> nav.navigate(BookDetail(book.id)) })
          }

Screens never see the controller. CatalogList gets an onOpen lambda, the state-hoisting pattern of State Hoisting applied to navigation, so it can be previewed and tested alone. To watch the stack, BookNest registers an OnDestinationChangedListener in a DisposableEffect (DisposableEffect and Cleanup) and logs each destination. Opening a book, pressing Back and opening another logged:

Output of 120
D/BookNestNav: now at com.example.booknest.ui.nav.BookDetail/{id} id=3
D/BookNestNav: now at com.example.booknest.ui.nav.Catalog
D/BookNestNav: now at com.example.booknest.ui.nav.BookDetail/{id} id=1

Each destination has a route, and each entry on the stack is a NavBackStackEntry that is also a LifecycleOwner, ViewModelStoreOwner and SavedStateRegistryOwner, so a screen's ViewModel lives exactly as long as its entry (ViewModel and UDF).