Navigation between screens
Add a second screen, give it an address, carry a value to it, and make the phone's Back gesture do the obvious thing — without writing a single line of Back handling yourself.
The version that almost works
Pocket Notes needs two screens: a list, and an editor. The obvious first attempt is a Boolean.
1var editing by remember { mutableStateOf(false) }
2
3if (editing) EditorScreen()
4else ListScreen(onOpen = { editing = true })Run it and it looks perfect. Tap a note, the editor appears. Then a user presses the phone's Back gesture, expecting to go back to the list, and the app closes.
Android has no idea you changed screens. As far as it is concerned there is one showing one thing, and Back means "leave".
You could handle Back yourself. Then you need to handle it for the third screen, and the fourth, and remember which one came from where, and cope with the phone being rotated in the middle. That bookkeeping has a name — the — and there is a library that does it properly.
Think about a stack of paper trays on a desk.
Opening a screen is putting a new sheet on top of the pile. You see the top sheet; everything else is still there, in order, underneath.
Pressing Back lifts the top sheet off and bins it. The sheet below is exactly as you left it — same notes in the margin, same coffee ring.
And when the pile is empty and you lift one more time, you have left the desk. That is why Back on the very first screen closes the app: there was nothing underneath.
Navigation is the library that manages the pile so you never have to.
Three pieces
Add one dependency — Pocket Notes and Focus Flow both have it already:
implementation(libs.androidx.navigation.compose)Then there are exactly three things to learn.
A is a piece of text that names a screen. "list". "settings". That is all a route is.
A is the composable that holds every screen and shows whichever one the current route names.
A is the object you ask to move. It keeps the back stack.
The smallest real example
1@Composable
2fun PocketNotesApp() {
3 val nav = rememberNavController()
4
5 NavHost(
6 navController = nav,
7 startDestination = "list"
8 ) {
9 composable("list") {
10 NoteListScreen(
11 onOpenNote = { nav.navigate("editor/$it") }
12 )
13 }
14 composable("editor/{noteId}") {
15 NoteEditorScreen(
16 onDone = { nav.popBackStack() }
17 )
18 }
19 }
20}rememberNavController() survives recomposition and rotation, so the pile of screens is not lost when the phone turns.
navigate("editor/3") pushes a sheet on. popBackStack() lifts one off. The system Back gesture already calls popBackStack() for you — you write nothing.
Tapping a card calls navigate("editor/3"). Back pops it off and returns here, scrolled exactly where you left it.
Carrying a value to the next screen
The editor needs to know which note. That value rides inside the route.
{noteId} in the route string is a slot. When you navigate to "editor/3", Navigation pulls 3 out of the address and hands it over.
Declare its type so a route expecting a number can never receive a word:
1composable(
2 route = "editor/{noteId}",
3 arguments = listOf(
4 navArgument("noteId") {
5 type = NavType.LongType
6 }
7 )
8) { entry ->
9 val id = entry.arguments
10 ?.getLong("noteId") ?: 0L
11 NoteEditorScreen(
12 vm = vm,
13 noteId = id,
14 onDone = { nav.popBackStack() }
15 )
16}That is the genuine code from Pocket Notes.
Pass ids, not objects. A route is text that Android may write down and restore later, so only small, simple values fit: numbers, short strings, booleans.
Sending a whole Note through a route means serialising it, and it means the editor is showing a stale copy from the moment it opens. Send the id; let the screen fetch the current version.
Pocket Notes uses "editor/0" for "a note that does not exist yet". id = 0 already means "new" everywhere else in the app — Room's autoGenerate treats zero as "please assign one" — so no separate "new" route is needed.
One idea, spelled the same way in three places. That is worth more than it sounds.
Sharing state between screens
Look at where the ViewModel is created in Pocket Notes:
1@Composable
2fun PocketNotesApp() {
3 val vm: NotesViewModel = viewModel()
4 val nav = rememberNavController()
5
6 NavHost(navController = nav, ...) { ... }
7}vm is created outside the NavHost, so both screens receive the same object — one ViewModel, one database connection, one source of truth. Save a note in the editor and the list behind it is already correct before you have finished the Back gesture, because Room pushed a new list to the they share.
Call viewModel() inside a composable { } block instead and you get a ViewModel scoped to that entry in the back stack — a fresh one per screen, cleared when the screen is popped. Focus Flow does exactly that, because its three tabs have nothing to share.
Both are correct. Choose by asking whether the screens need the same facts.
Tabs are different
Focus Flow has a bottom bar: Timer, Stats, Settings. If tapping a tab simply called navigate(), every tap would push another sheet on the pile, and Back would walk you through your entire tab history like a browser.
1private fun openTab(
2 nav: NavHostController,
3 dest: Destination,
4) {
5 nav.navigate(dest.route) {
6 popUpTo(nav.graph.startDestinationId) {
7 saveState = true
8 }
9 launchSingleTop = true
10 restoreState = true
11 }
12}Highlighting the current tab
The bar has to know which tab you are on, so it watches the back stack:
1val entry by nav.currentBackStackEntryFlow
2 .collectAsState(initial = null)
3val route = entry?.destination?.route
4 ?: Destination.TIMER.routeA of "what is on top right now", collected into state. Everything you learned in Lesson 4.3 applies here unchanged — the navigation state is just more state.
You will add a third screen to Pocket Notes and reach it from the list.
- Open Pocket Studio, tap Projects, then Pocket Notes.
- Tap Editor and open
MainActivity.kt. - Inside the
NavHost { }block, below the existingcomposable("editor/...")entry, add a new one:composable("about") { Text("Pocket Notes 1.0") } - Open
ui/NoteListScreen.kt. In theLargeTopAppBar, add anactions = { }parameter containingTextButton(onClick = onAbout) { Text("About") }. - Add
onAbout: () -> Unitas a parameter ofNoteListScreen. - Back in
MainActivity.kt, passonAbout = { nav.navigate("about") }. - Tap Run. Tap About, then use the phone's Back gesture. You return to the list, scrolled exactly where you were — and you wrote no Back code at all.
- Press Back once more. The pile is now empty, so the app closes. That is correct behaviour.
composable { } declares. Nearly always a spelling mismatch between the route you built and the route you registered."editor/{noteId}" matches "editor/3"; "editor{noteId}" and "Editor/{noteId}" match nothing. Declaring route strings as constants stops this happening twice.NavType.LongType but something read it as a different type, or built the route with a value that is not a whole number.getLong("noteId"), not getString. And check the value you interpolate into the route is a Long — "editor/$note" puts the whole object in the address by accident.NavHost had registered them — typically a navigate() fired from a composable that runs before the host is composed.LaunchedEffect. Never call navigate() directly in the body of a composable, because that body runs during drawing.composable, and the one that builds a route lives in the navigation package.androidx.navigation.compose.composable and androidx.navigation.compose.NavHost. If neither is offered, the navigation dependency is missing — add it and tap Sync.popUpTo with inclusive = true removed the screen you wanted to go back to, or you used navigate where you meant popBackStack.popBackStack(). Only use popUpTo when you genuinely want to erase history — after a login screen, for example, which nobody should be able to return to.- Handling Back with a Boolean breaks immediately. The is real bookkeeping, and the Navigation library owns it.
- Three pieces: a is text, a shows the matching screen, a moves between them.
navigate("editor/3")pushes;popBackStack()pops; the system Back gesture pops for free.- Declare a with
navArgument, and pass ids, not objects. - Create the ViewModel outside the
NavHostto share it; inside acomposableblock to scope it to one screen. - Tabs need
popUpTo,saveState,launchSingleTopandrestoreState— all four, or the bottom bar feels wrong. - Next: the notes themselves. gives you a real database, so what the user writes is still there tomorrow.