Pocket Studio Academy
HomePart 55.9

Pocket Notes 4 — the editor screen

Full course16 min read·4 questions

A second screen, and the road between them. A route is just a piece of text, the + button stops lying, and the editor has no Save button on purpose — leaving it saves, which means catching both ways out.

The button that has been lying to you

The + button in the corner of Pocket Notes has been fake since chapter 2. Tap it and you get a note called "Test note" with a body that says "Saved straight to SQLite." Useful for proving the database works. Completely useless for writing anything down.

This chapter makes it real, and doing that needs three things the app has never had: a second screen, a road between the two screens, and a decision about when a note gets saved.

The first two are mechanical. The third is a design choice, and it is the most interesting thing in this lesson.

Think of it like this

Think about a paper notebook on a kitchen table.

You open it, you write "milk, bread, batteries", you close it. At no point do you press anything to keep the words. Closing a notebook is not a risky act. Nobody has ever shut a notebook and wondered whether their shopping list survived.

Now imagine a notebook with a small button on the cover, and a rule: anything you wrote since you last pressed the button disappears when you close it. You would not buy that notebook. You would not lend it to anyone. And yet that is exactly how most note apps behaved for about twenty years.

Pocket Notes has no Save button because paper does not have one. Leaving the screen is the save.

One dependency

Two screens need a way to get between them.

app/build.gradle.ktskts
1    implementation(libs.androidx.activity.compose)
2    implementation(libs.androidx.navigation.compose)

is small — a controller, a back stack, and a way to describe screens by name. It is not a framework; it is a lookup table with history.

A route is a piece of text

That is the whole idea, and it is worth stopping on for a second because people expect it to be harder.

A is a String. "list" means the list screen. "editor/7" means the editor showing note 7. "editor/0" means the editor showing a note that does not exist yet — a new one — because chapter 1 decided that id = 0 is what an unsaved Note looks like.

Going to a screen is nav.navigate("editor/7"). Coming back is . There is nothing else.

The ViewModel grows up

addTestNote() is deleted. Two real functions replace it.

The list reports upward

NoteListScreen changes by four lines. Its signature grows two lambdas:

ui/NoteListScreen.ktkotlin
1@OptIn(ExperimentalMaterial3Api::class)
2@Composable
3fun NoteListScreen(
4    vm: NotesViewModel,
5    onOpenNote: (Long) -> Unit,
6    onNewNote: () -> Unit
7) {
8    val state by vm.uiState.collectAsState()

The stops calling the ViewModel and starts calling upward:

ui/NoteListScreen.ktkotlin
1            FloatingActionButton(
2                onClick = onNewNote,

and the real lambda finally reaches the cards:

ui/NoteListScreen.ktkotlin
1                NoteList(
2                    state = state,
3                    onOpenNote = onOpenNote
4                )

NoteCard has been calling onClick(note.id) since chapter 3 and that call has been going nowhere. Now it arrives. This is applied to events rather than values: the screen that knows a tap happened is not the screen that knows what a tap should do.

The editor

One new file, and the most interesting composable in Pocket Notes.

What it remembers

The two ways out

Here is the heart of the chapter.

Why no Save button? Because a Save button does not remove the question, it moves it. Every app with one also needs an answer to "you have unsaved changes" — which means a dialog, which means interrupting somebody who was trying to leave, to ask them a question they should never have had to think about.

Saving on the way out deletes the whole category of problem. The cost is that you must catch every way out, and the guard against empty notes has to be right. Both are five lines. The dialog is not.

Fields that look like paper

ui/NoteEditorScreen.ktkotlin
1    // Borderless fields: the note is the page, not a
2    // form. Only the cursor keeps the accent colour.
3    val fieldColors = TextFieldDefaults.colors(
4        focusedContainerColor = Color.Transparent,
5        unfocusedContainerColor = Color.Transparent,
6        focusedIndicatorColor = Color.Transparent,
7        unfocusedIndicatorColor = Color.Transparent,
8        cursorColor = colors.primary
9    )

A Material normally has a filled grey box behind it and an underline that thickens when focused. Both are excellent on a sign-up form, where you want to say there are three separate things to fill in here. Both are wrong on a note, where there is only one thing — the writing — and every box you draw round it makes it look more like paperwork.

Four colours go transparent. The cursor keeps the app's honey #8A5100, because a cursor you cannot find is a genuine problem rather than a stylistic one.

The app bar above them is deliberately plain:

ui/NoteEditorScreen.ktkotlin
1            TopAppBar(
2                title = {
3                    Text(
4                        text = if (isNew) "New note"
5                        else "Edit note",
6                        style = type.titleMedium
7                    )
8                },
9                navigationIcon = {
10                    IconButton(onClick = { finish() }) {
11                        Icon(
12                            imageVector =
13                                Icons.Default.Close,
14                            contentDescription =
15                                "Save and close"
16                        )
17                    }
18                },

The is "Save and close", not "Close". A ✕ usually means discard, and users get no other clue about which one this is. Naming the actual behaviour costs one word and removes an entirely reasonable fear.

9:41▲ ▮
New note
Title
Start writing…

Tapping + opens a new note, focused, with the cursor in the title.

9:41▲ ▮
Edit note
Shopping
Oat milk, tomatoes, bread
Batteries — the small round ones
Ask about the bike lock

The same screen opened from a card. The app bar says Edit note.

Try it in Pocket Studio

One new file, three changed. Start from your chapter 3 project or the chapter 3 ZIP.

  1. Open Pocket StudioProjectsPocket Notes.
  2. Open app/build.gradle.kts, add implementation(libs.androidx.navigation.compose) under the activity.compose line, and tap Build.
  3. Open ui/NotesViewModel.kt. Delete addTestNote() entirely and add load(...) and save(...) in its place.
  4. In the ui package, tap NewKotlin File and name it NoteEditorScreen. Type the whole file.
  5. Open ui/NoteListScreen.kt. Add onOpenNote and onNewNote to the signature, change the FAB's onClick to onNewNote, and pass onOpenNote down to NoteList.
  6. Open MainActivity.kt and replace PocketNotesApp() with the NavHost version. Add the five navigation imports.
  7. Tap Run. Tap +. The editor should open with "New note" in the bar and the keyboard already up.
  8. Type a title and a couple of lines, then tap the . You land back on the list with your note at the top of it.
  9. Tap that card. The editor opens with your words in it and "Edit note" in the bar. Add a word, then use the system back gesture instead of the ✕. The change is still there.
  10. Now comment out the BackHandler { finish() } line with // and Run again. Edit a note, leave with the back gesture, and the edit is gone. Put the line back — that is Error Doctor entry 5, seen from the outside.
  11. Tap + and immediately tap without typing anything. No new card appears. Now delete the if (cleanTitle.isEmpty() && ...) block from save(), Run, and do it three times. Three blank cards. Put the guard back and delete the blanks by hand — you do not have swipe-to-delete until the next chapter.

Pocket Notes — end of chapter 4

A complete project. Unzip it, open it in Pocket Studio, and press Run.

Download ZIP
Error Doctor5 common errors
java.lang.IllegalArgumentException: Navigation destination that matches request NavDeepLinkRequest{ uri=android-app://androidx.navigation/editor/3 } cannot be found in the navigation graph NavGraph(0x0) startDestination={Destination(0x…) route=list}
MeansA crash the moment you tap a note. You called nav.navigate("editor/3"), and no route in the graph matches that shape. Usually a typo, a missing {noteId}, or a composable("editor") declared with no argument at all.
FixThe pattern and the call have to line up exactly: composable(route = "editor/{noteId}", ...) against nav.navigate("editor/$id"). Read the URI in the message — it shows precisely what was asked for, which makes the mismatch obvious.
java.lang.IllegalStateException: Wrong argument type for 'noteId' in argument bundle. long expected.
MeansYou read the argument with getLong but declared it as a String — or declared nothing, which defaults to a string. Routes are text, so the type has to be stated or everything in them stays text.
FixDeclare it once with navArgument("noteId") { type = NavType.LongType } and read it the matching way with entry.arguments?.getLong("noteId") ?: 0L. Change one and you must change the other.
Typing is lost when the phone rotates — no error message at all
MeansThe text was held in remember { mutableStateOf("") }. remember survives redraws but not a : on rotation the whole Activity is destroyed and rebuilt, and everything held only in memory goes with it.
FixrememberSaveable { mutableStateOf("") } for title, body and loaded. It stores the value in the saved-state bundle, which Android hands back to the new Activity.
After rotating, the note reverts to its last saved text — again, no error
MeansrememberSaveable did bring your edits back, and then LaunchedEffect(noteId) ran a second time in the rebuilt Activity and copied the stored note straight over them.
FixThe loaded flag, itself saved: LaunchedEffect(noteId) { if (!loaded) { ... ; loaded = true } }. The database gets exactly one turn per note. This bug and the one above look identical from the outside and have opposite causes, which is why they are two entries.
Leaving with the system back gesture does not save, but the ✕ button does
MeansThe ✕ calls your finish(). Back does not — it goes to the navigation library, which pops the screen without asking your code anything.
FixBackHandler { finish() }, with import androidx.activity.compose.BackHandler. It only intercepts while this screen is the one on top, so it will not steal Back from the list screen underneath.
Recap
  • A is a String. "editor/7" is the editor showing note 7, and "editor/0" is a new one — because id = 0 already meant "unsaved".
  • Create the above the so both screens share one database connection and one .
  • Declare a with a type, and read it back the matching way. Getting that pair out of step is a runtime crash, not a compile error.
  • Text you can type must be in — and so must the loaded flag that stops the database overwriting it after a rotation.
  • The editor saves on the way out, so both exits have to go through one finish(): the ✕ button and for the system back gesture.
  • A note with nothing in it is not saved. That single if in the ViewModel is what makes save-on-exit safe rather than messy.
  • means one save function covers both new notes and edits, with no branch anywhere in the app.
  • Next: deleting. A swipe to the left removes a note straight away — and a snackbar with an Undo button makes that safe to do without ever asking "are you sure?".