Flutter / Asynchronous application boundaries / Lesson 10
Navigation and route results
Give routes explicit inputs and results, and distinguish local stack transitions from URL-driven application state.
On this page
Lesson goals
- A route is an application boundary
- Select a navigation model that fits the application
- Receive a typed local result
A route is an application boundary
A route introduces an interaction lifetime and often an input identity. Passing an entire mutable screen object across that boundary hides dependencies and can retain obsolete data. Prefer a stable identifier or a small immutable input contract, then let the appropriate owner resolve current data. Distinguish opening a detail view from authorizing access to its record: navigation visibility does not enforce permission. A route opened by a deep link may bypass the screen that normally supplied its inputs, so validate missing, malformed, and unavailable identifiers at the route boundary.
Select a navigation model that fits the application
Navigator manages a stack of routes and works well for local push-and-return interactions. Applications with browser URLs, deep links, nested navigation, or state restoration may need a Router-based integration with explicit route parsing and stack policy. A routing package can coordinate those details, but its compatibility and lifecycle rules still need review. Avoid assuming that every native push naturally produces the desired web history behavior. Write down what Back, reload, direct entry, and an unknown URL should do before choosing an abstraction or adding special cases to individual buttons.
Receive a typed local result
The first fragment opens a local chooser and awaits its optional String result. Cancellation is represented by null, so it does not overwrite the existing selection. The mounted check protects the caller if its owner disappears while the chooser is open. The destination is expected to return one of its permitted identifiers, but the receiving domain owner should still validate an identifier before a consequential command. This pattern is appropriate for a small local interaction; it does not establish deep-link parsing or persistent URL state.
Future<void> chooseWorkshop() async {
final id = await Navigator.of(context).push<String>(
MaterialPageRoute<String>(builder: (_) => const WorkshopChooser()),
);
if (!mounted || id == null) return;
setState(() { selectedId = id; });
}
// WorkshopChooser returns a choice with:
// Navigator.of(context).pop<String>('w-7');
Choosing w-7 updates selectedId; cancelling leaves the old selection unchanged.
The surrounding State defines selectedId and the application supplies WorkshopChooser. No route is opened in the authoring environment.
Parse route identity independently
The second fragment extracts one workshop identifier from a URI. It accepts exactly the chosen path shape and a narrow synthetic identifier format; it does not treat arbitrary path text as a database key. A real application may define a richer schema, but it should still separate parsing from data lookup and permission checks. Decide how encoded characters, trailing slashes, query parameters, and unsupported versions are handled. Returning a failed parse is preferable to inventing a default record that could send the user to a plausible but incorrect destination.
String? workshopId(Uri uri) {
final parts = uri.pathSegments;
if (parts.length != 2 || parts[0] != 'workshops') return null;
return RegExp(r'^w-[0-9]+$').hasMatch(parts[1]) ? parts[1] : null;
}
void main() {
print(workshopId(Uri.parse('/workshops/w-7')));
print(workshopId(Uri.parse('/workshops/unknown')));
}
w-7
null
The parser establishes only route syntax. It does not prove the record exists or the current user may access it.
Keep data lifetime separate from route lifetime
Popping a screen should dispose its local controllers and listeners, but it need not erase shared application data. Conversely, keeping a repository alive should not keep every old screen context alive. A route result can request a refresh or provide a small confirmed value instead of transporting the entire mutable view model back through the stack. When unsaved work exists, define a clear leave policy using the current navigation APIs supported by the project. Test both explicit cancel controls and system or browser Back because users do not always leave through the button you designed.
Verify entry and exit paths
A navigation test matrix should include normal opening, cancellation, selection, direct entry, unknown input, unavailable data, and permission failure. Check that Back returns to a meaningful location without duplicating screens. For web targets, verify reload and URL history in the actual routing host; a pure URI test cannot establish browser integration. Route changes should also preserve sensible focus and announcements so assistive technology users understand that context changed. Keep platform-specific behavior claims scoped to the target and host actually exercised.
Common errors and fixes
Cancelling a chooser erases the old selection
- Why it happens
- The optional null result was treated as a new selected value.
- How to fix it
- Handle cancellation before committing the result.
- Prevent it next time
- Test Back and explicit cancel separately.
A deep link opens the wrong default record
- Why it happens
- Invalid route input silently fell back to an arbitrary identifier.
- How to fix it
- Render a clear invalid or missing-route state.
- Prevent it next time
- Test malformed and unsupported path shapes.
A route identifier is mistaken for authorization
- Why it happens
- Parsing and access control were conflated.
- How to fix it
- Check permission at the authoritative data or command boundary.
- Prevent it next time
- Exercise direct entry without the normal menu path.
Check your understanding
1. Should a detail route receive a mutable list index or a stable record identifier?
Show worked solution
Use the stable identifier. The list may reorder or refresh while the detail is open. Resolve the identifier under the data owner and present an unavailable state if it no longer exists. The route then remains meaningful independently of one list snapshot.
2. What extra evidence is required before claiming the URI parser supports browser deep links?
Show worked solution
Integrate it with the chosen Router host and verify direct URL entry, reload, unknown paths, and Back or Forward history in that target. The pure parser only recognizes a string shape; server fallback configuration and browser navigation are separate boundaries.
Key takeaways
- A route is an application boundary
- Verify entry and exit paths