Navigation
In an app with a router, every screen that a feature declares has a route, and the router role generates a typed way to reach it: context.nav. Screens navigate through it instead of through go_router, so their code works with any router.
Paths and names
A feature declares its routes relative to its own namespace, the path /<feature id>. The feature home declares the route /, so its full path is /home. Its full name, home.home, joins the id of the feature and the name of the route.
The examples on this page use a feature catalog that declares two routes: a list at / and, below it, the details of an item at item/:id, which takes the path parameter id, an int, and the optional query parameter color, a String. Their full paths are /catalog and /catalog/item/:id, and their full names catalog.list and catalog.item.
context.nav
context.nav has a getter for each feature and a method for each of its routes, with the parameters of the route:
context.nav.catalog.list()
context.nav.catalog.item(id: 5, color: 'red')
Each returns a NavLink, which navigates in one of three ways:
| Method | What it does |
|---|---|
go() | Shows the location with the chain of its parents below it as the stack: go() to an item shows the list below it, so Back returns to the list. |
push<T>() | Shows the location on top of the current stack. Its Future completes with the value the page returns when it closes. |
replace() | Replaces the page on top of the stack with the location. |
TextButton(
onPressed: () async {
final color = await context.nav.catalog.item(id: 5).push<String>();
// The item page closed, returning a color or null.
},
child: const Text('Item 5'),
)
Behind the methods is a location class for every route, generated into lib/core/router/navigation.dart:
/// The location of `catalog.item`: `/catalog/item/:id`.
final class CatalogItemLocation extends AppLocation {
/// Creates the location with the values of the route.
const CatalogItemLocation({required this.id, this.color});
/// The value of the path parameter `id`.
final int id;
/// The value of the query parameter `color`.
final String? color;
String get routeName => 'catalog.item';
String get path => _withQuery('/catalog/item/$id', {'color': color});
AppLocation get parent => const CatalogListLocation();
}
context.nav works with any context of the app, even one above the router, because the generated router navigates itself rather than looking up the router of the context.
Parameters
A route takes values from its location. Path parameters, such as :id, are always required, and query parameters, such as ?color=red, can be required or optional. A value is a String, an int, a double or a bool. The screen of the route takes each as a named parameter of its constructor, nullable when it is optional:
const ItemScreen({required this.id, this.color, super.key});
With go_router, the router parses an int or a double from the location with tryParse, and a bool is true or false exactly. When a required value is missing or has the wrong type, as in /catalog/item/abc, the location shows the error screen of go_router instead of the page. An optional value that is missing or has the wrong type reaches the screen as null.
The start screen
A feature can mark a route as one that the app can start on, as home does with /home. The app opens on it, and the path / redirects there. When several routes can start the app, such as /home and /catalog, smf create asks which one:
? Which screen does the app start on? (↑↓ to move, enter to choose)
❯ /home (home)
/catalog (catalog)
A run without a terminal takes the full path from --start, such as --start /catalog, and stops with a usage error without it. --start can also name a route that no feature marks, as long as the route has no required parameters, and in a terminal it answers the question in advance. When no route can start the app, / shows the fallback start screen with the name of the app. If the app has routes but no feature marks one, smf create warns and suggests --start.
Tabs and other main navigation
With a layout, such as bottom_tabs, the routes that features mark as destinations form the main navigation of the app: a tab for each, in the order of the features. The app opens on the tab of its start screen.
- Every tab keeps its own stack while another tab is selected. The routes below a destination stay in its tab.
- The other top-level routes are outside the main navigation. They show over the tabs, and going to one leaves the tabs behind.
go()to a location in a tab selects that tab and makes the chain of the location its stack.go()to a location outside the main navigation replaces the whole stack with its chain.push()from a page in a tab shows the location on top of the selected tab's stack, whichever tab the location belongs to, and the selected tab stays.- The app shows the main navigation only once. So on a page shown over it,
push()andreplace()of a location in the main navigation throw aStateErrorthat says to usego()instead, and the stack stays as it is. On a page with no main navigation below it,push()brings the main navigation back on top with the location.
Following the screens
Modules can follow the screen the user sees through the router, whichever router it is. firebase_analytics logs a screen view for each screen, with the full name of its route, such as home.home. Switching tabs counts as seeing another screen. Pages that the app shows with a navigator directly, such as with Navigator.push, and dialogs are not screens of the router.
Routes of your own
SMF generates the navigation from the routes of the modules when it creates the app. Screens you add later are yours to route: add a GoRoute to lib/core/router/app_router_factory.dart and navigate to it with go_router, as in any go_router app.