Pocket Notes 3 — the note list and the empty state
Turn a plain column of text into a real list: cards, human-readable timestamps, a title that shrinks as you scroll, and the screen everybody forgets to design — the one with nothing on it.
The screen you see most, and the screen you see first
Two screens matter more than any other in a notes app.
The list is the one you look at hundreds of times. It has to survive four hundred notes without getting slow, and it has to be readable at a glance, which means a preview and a timestamp that says "3 h ago" rather than 1755262800000.
The empty screen is the one you see once — on the very first launch, before you have written anything. It is the app's first impression, and in most apps it is a blank white void that makes you wonder whether something crashed.
This chapter builds both, and by the end the app finally looks like an app.
Think about a roll of paper towels.
The plain Column you wrote in chapter 2 is what happens if you unroll the whole thing across the kitchen floor to find one sheet. It works. With four sheets it is instant. With four hundred you have a paper towel problem.
A keeps the roll rolled up. Only the few sheets actually passing your hand are unrolled, and the ones you scroll past get rolled away again behind you.
The list can be a thousand notes long. Compose still only ever builds the dozen you can see.
One new dependency
The empty state needs a pencil-on-paper icon, and the list needs a plus. The handful of icons bundled with Material 3 does not include the first one, so this chapter adds the full set.
1 implementation(libs.androidx.material3)
2 implementation(
3 libs.androidx.material.icons.extended
4 )That artifact is large — a few thousand vector icons — but only the ones you actually reference end up in the , because strips the rest out of a release build.
Milliseconds are for computers
updatedAt is a Long. Nobody wants to read a Long.
That java.time package is the reason this app sets minSdk = 26. It arrived with Android 8.0, and on anything older the build fails outright — the last entry in the Error Doctor below.
A note, drawn as a card
The screen with nothing on it
A blank screen is a screen you shipped without thinking about. An is a screen you designed.
The rules are the same every time: a symbol so the eye has somewhere to land, a headline that is not an apology, and one sentence saying what to do next.
1@Composable
2fun EmptyNotes(modifier: Modifier = Modifier) {
3 EmptyMessage(
4 icon = Icons.Outlined.EditNote,
5 title = "No notes yet",
6 body = "Tap the + button to write your first " +
7 "one. Everything you type is saved on " +
8 "this phone, and only on this phone.",
9 modifier = modifier
10 )
11}EmptyNotes is a thin wrapper around a private composable called EmptyMessage, which takes an icon, a headline and a sentence. That split looks like over-engineering for one empty state. It is not: chapter 6 adds a second one for "your search found nothing", and it will be six lines because of this decision.
The layout underneath is a centred Column with three things in it:
1 Surface(
2 shape = CircleShape,
3 color = colors.primaryContainer,
4 modifier = Modifier.size(96.dp)
5 ) {
6 Box(
7 contentAlignment = Alignment.Center
8 ) {
9 Icon(
10 imageVector = icon,
11 contentDescription = null,
12 tint = colors.onPrimaryContainer,
13 modifier = Modifier.size(44.dp)
14 )
15 }
16 }A 96dp Surface clipped to a circle, filled with primaryContainer — the soft honey #FFDDB8 — with a 44dp icon centred inside it. contentDescription = null is correct here and not laziness: the icon says nothing the headline underneath does not already say, so announcing it twice to a screen reader is noise.
At the very bottom of that Column sits one more line:
1 // Nudges the block above dead centre, which
2 // reads better than true centre.
3 Spacer(Modifier.height(64.dp))A 64dp spacer after everything, inside a vertically centred column, pushes the whole block up by 32dp. Mathematically centred and optically centred are not the same thing, and every designer you will ever work with knows it.
The list screen, rebuilt
That nested if is the payoff for putting loading in NotesUiState back in chapter 2. It is a single frame of wrongness that would be almost impossible to photograph and very easy to feel.
The lazy part
The list. Card 2 shows the two-line clamp and its ellipsis.
First launch. Nothing in the database, and still a designed screen.
The app bar and the stay on screen behind the empty state, because they belong to the Scaffold and the empty state is only the content slot.
Three new files and one rewrite. The FAB still adds test notes — that changes next chapter.
- Open Pocket Studio → Projects → Pocket Notes.
- Open
app/build.gradle.ktsand add the wrappedlibs.androidx.material.icons.extendeddependency. Tap Build. - In the
uipackage, tap New → Kotlin File three times:RelativeTime,NoteCard,EmptyNotes. - Open
NoteListScreen.ktand replace the whole file with the chapter 3 version. - Tap Run. If you still have test notes from chapter 2, you get the card list. If not, you get the empty state.
- Press the + a few times, then scroll the list up and down. Watch the big title shrink into a normal bar and the bar change colour.
- Find the
.nestedScroll(bar.nestedScrollConnection)line and comment it out with//. Run again and scroll. The title never moves — that is exactly what the fourth Error Doctor entry looks like from the outside. - Put the line back.
Pocket Notes — end of chapter 3
A complete project. Unzip it, open it in Pocket Studio, and press Run.
LargeTopAppBar, TopAppBarDefaults and rememberTopAppBarState are not final API yet, and Compose will not let you use them silently. Expect eight or ten of these at once — one per usage.@OptIn(ExperimentalMaterial3Api::class), immediately before @Composable. It silences all of them at once.items is an extension function on LazyListScope, not a member of LazyColumn. Importing LazyColumn alone does not bring it along.import androidx.compose.foundation.lazy.LazyColumn and import androidx.compose.foundation.lazy.items.Note starts life with id = 0, so a list holding two of them has two rows claiming the same identity.key = { note -> note.id } — and make sure the list you are drawing came from the database, where Room guarantees the ids are unique.scrollBehavior, passed it to LargeTopAppBar, and never told the Scaffold about the scrolling. The bar is listening to a wire that was never connected.modifier = Modifier.nestedScroll(bar.nestedScrollConnection).java.time only exists on Android 8.0 and above, and your minSdk is lower than that.minSdk = 26 in app/build.gradle.kts, which is exactly what this project does and exactly why. If you must support older phones, either use java.text.SimpleDateFormat or turn on core library .- A only builds the rows on screen, so a list of a thousand notes costs the same as a list of twelve.
- Give
itemsa from the database id. Without it Compose tracks rows by position, and deleting one confuses everything below it. - separate by colour, not . The click goes on the inner
Columnso the respects the rounded corners. - Raw milliseconds are for computers. A short
whenladder turns them into "Just now", "3 h ago" and "Yesterday". - An is a designed screen: a symbol, a headline and one instruction. Check
loadingfirst, or you will flash it at people who have hundreds of notes. - Next: the + button still adds fake notes. Chapter 4 adds a second screen, a route between them, and an editor that saves when you leave it.