Navigation
By the end of this lesson
Structure navigation that matches platform expectations.
One screen is visible at a time, so the user builds a mental map of your app from what they can reach and how they got there. Navigation is that map. Get it wrong and people cannot find features that exist.
There is a second reason to take it seriously. Both platforms have long-established conventions for moving backwards, and users have them in their fingers. Nobody reads your app's navigation model; they apply the one they already know. An app that contradicts it feels broken even when every screen works.
Three patterns cover most applications. They combine, and knowing what each is for prevents the usual mess of all three at once:
- Stack
- Screens pushed on top of each other, newest visible, and popping returns to the previous one. This is how you go deeper into content: site list, then an employee, then their visit history. Suits any drill-down journey and any temporary task. It is the wrong shape for switching between unrelated areas, because the stack grows forever and nothing feels like home.
- Tabs
- Two to five top-level destinations, always visible, each keeping its own stack. Suits an app with a small number of equally important areas — Today, Team, Notes. Switching is instant and the user can see everything on offer. It fails past about five items, where labels shrink and the bar becomes a puzzle.
- Drawer
- A panel sliding in from the edge holding a longer list of destinations. Suits apps with many sections, most of which are visited rarely. The cost is discoverability: what is hidden gets used far less, so a drawer is a poor home for anything central. Putting your main feature in a drawer is how it becomes the feature nobody found.
Going backwards is a platform contract
Android users go back with a system gesture or button that works everywhere, in every app, including yours. It is not your control and you do not get to disable it. Your job is to have a sensible answer for it on every screen: pop the stack, close the sheet, dismiss the keyboard. When there is nothing left to pop on a root screen, it leaves your app, and that is correct behaviour rather than a bug to prevent.
iOS has no system back button, so each screen supplies a back control in the navigation bar, and the edge swipe from the left does the same thing. If you replace the standard bar with your own design, the swipe frequently stops working, and users who rely on it will conclude your app is stuck.
Handle both, and let the same logic serve both. One function that answers "what does backwards mean here" is called by the Android system back and the iOS back control alike. Two divergent implementations drift, and the divergence shows up as a platform-specific bug nobody can reproduce.
There is one pattern to avoid: quietly repurposing back as cancel. If going back from a half-written visit note discards it without a word, the user who reflexively swiped has lost their work to a gesture they use a hundred times a day for something harmless. Either save the draft, or ask. Preferably save the draft.
type Screen =
| { name: "siteList" }
| { name: "employeeDetail"; employeeId: number }
| { name: "visitNotes"; employeeId: number };
const HOME: Screen[] = [{ name: "siteList" }];
/** Turns an external path into the stack the user should arrive on. */
export function resolveLink(path: string): Screen[] {
const parts = path.split("/").filter((part) => part.length > 0);
if (parts[0] !== "employees") return HOME;
if (parts.length === 1) return HOME;
const employeeId = Number(parts[1]);
if (!Number.isInteger(employeeId) || employeeId <= 0) return HOME;
const stack: Screen[] = [...HOME, { name: "employeeDetail", employeeId }];
if (parts[2] === "notes") {
stack.push({ name: "visitNotes", employeeId });
}
return stack;
}
// "/employees/482/notes" arrives on the notes screen, and back still works.- The shape matters more than the API. Every navigation library expresses this differently; what carries across is that a link resolves to an array of screens rather than a single destination.
- That array is the whole point. Land the user directly on the notes screen with an empty stack behind it and their first back gesture drops them out of the app. Land them on the same screen with the site list underneath and they can explore from where they arrived, which is what they expect.
- The path is untrusted input. It can come from a notification, a text message, a QR code on a piece of equipment, or someone typing. Number("abc") produces NaN and Number("") produces zero, so both the integer check and the positive check are doing real work here.
- Unknown paths fall back to a known screen instead of throwing. Links outlive app versions: a colleague shares a link from a newer build, or an old notification arrives after a redesign. A predictable fallback beats a crash on launch, and a crash on launch from a link is a particularly hard failure to diagnose.
- Keeping resolution in one pure function makes it directly testable. You can assert the resolved stack for a dozen paths in milliseconds, with no device and no navigator involved.
Deep linking is what makes a notification or a shared link open the right screen. Six things to get right, in roughly this order:
Declare the links the app handles
Each platform has its own registration, and for https links both require a file hosted on your domain that confirms the app may claim them. Without that association the link opens a browser instead of your app, and the mistake is easy to miss because a custom scheme like fieldapp:// keeps working.
Map a path to a screen stack
One function, as above, from path to an array of screens. Keep it away from the navigator so it can be unit tested. This is also where you decide what a partially recognised path means.
Validate everything the link carries
Identifiers and parameters arriving from outside the app are input like any other. Check types and ranges before using them, and never let a link value reach a request or a storage key unchecked.
Decide what happens when nobody is signed in
Hold the target, show sign-in, then continue to it. Dropping the user on the home screen after they authenticate wastes the link entirely, and it is the most common deep linking failure in production apps.
Handle cold launch and warm resume separately
A link that starts the app arrives as part of launch, while a link that arrives with the app already running comes through a listener. Both paths have to reach the same resolver, and each needs its own test, because getting one right proves nothing about the other.
Check the permission after resolving
A link naming employee 482 does not entitle the person holding the device to see employee 482. The screen loads from the API, the API decides, and the app shows a clear refusal rather than an empty page. Never treat the existence of a link as authorisation.
Summary
- Stacks are for drilling in, tabs for a few equally important areas, drawers for many rarely used ones
- Backwards is a platform contract: answer the Android system back and the iOS back control with the same logic, and never let it silently discard the user's work
- Resolve a deep link to a whole screen stack so back has somewhere to go, and validate everything the link carries
- Handle cold launch and warm resume separately, and hold the link target across sign-in
- A link is not authorisation — the API still decides what the person holding the device may see
Practice
Attempt each one before opening the solution. Getting it wrong first is how the idea sticks.
Think about it
Choose a structure and defend it
The field app has five areas: today's visits, the site team directory, visit notes, a scanner for equipment QR codes, and settings.
Pick a navigation structure. Say what you put at the top level, what is reached by drilling in, and where the scanner goes. Then say what you would change if two more areas were added next quarter.
Show solution
A reasonable answer: tabs for Today, Team and Notes, each with its own stack. Those are the three things an engineer uses daily, and constant switching between them argues for them being one tap apart.
Settings does not deserve a tab. It is visited rarely, so an entry point on a profile screen or in the top corner is enough. Spending a top-level slot on it pushes something used hourly further away.
The scanner is the interesting one, and there is more than one defensible answer. If scanning starts most visits, it belongs as a prominent action on the Today screen, in thumb reach. If it is occasional, an action in the navigation bar is fine. What it is not is a tab, because a tab implies a place you go to rather than a tool you use, and a camera screen is a poor place to be left sitting.
With two more areas, tabs stop scaling. Resist the reflex to add a fifth and sixth tab or to move everything into a drawer. Ask instead whether the new areas are genuinely top-level or belong inside an existing one — most turn out to be screens within Team or Notes. If they truly are separate, then a drawer alongside a reduced tab bar, with the daily three still on the bar, keeps the frequent journeys short.
The principle underneath: top-level slots are scarce and should go to frequency, not to importance as judged by an org chart.
Try it yourself
Test the resolver, then the two arrival paths
Write a resolver like the one in this lesson for your own app or a practice one, and unit test it with these paths: a valid detail path, a valid nested path, a path with a non-numeric id, an empty path, and a path naming a section that does not exist.
Then, on a device, open the app from a link twice: once with the app not running, and once with it already in the foreground.
Show solution
The unit tests are quick because the resolver is a pure function, and they cover exactly the cases that reach you from the outside world where you have no control over the input.
The two device checks matter because they exercise different code. A cold launch receives the link as part of starting up, when your navigator may not exist yet, so a link handled too early is dropped. A warm resume arrives through a listener while a stack is already on screen, and the question becomes whether you replace that stack or push onto it.
Most teams test one and ship the other broken. The cold path usually fails, because it is the one where ordering with app startup matters and the timing differs between a debug build and a real launch.
Knowledge check
Nothing is recorded and there is no score. The explanation appears either way.
Saved in this browser only.