go_router
| Package | Kind | Provides the role | Needs | Choose with |
|---|---|---|---|---|
smf_go_router | infrastructure | Router | nothing | -m go_router |
go_router routes the app with go_router 17. The features of the app declare their routes for the router role, which generates the typed navigation, context.nav, and this module turns the routes into a GoRouter.
What it adds to the app
go_router: ^17.5.0in the dependencies.lib/core/router/app_router_factory.dartwithcreateAppRouter(), which creates the router of the app once, on first use: a_GoRouter, a class of that file that extendsGoRouter.
The router role adds, whichever module provides it, lib/core/router/app_router.dart with AppRouter and appRouter, and lib/core/router/navigation.dart with the location classes and context.nav. With a router, the root widget of the app becomes a MaterialApp.router.
In the guide for coding agents of the app, the router role describes navigation and the three places of a route under "Router". The module adds how its file writes a route as a GoRoute, that initialLocation and the route / change together, and, in an app with a layout, where the destinations of the main navigation go and that the router creates the route of the main navigation anew.
The routes
- Every route is a
GoRouteunder the namespace of its feature, such as/homeor/catalog/item/:id, with its children below it. - The name of a route is its full name, such as
home.home. The listeners of the screen get it as the name of the screen, and navigator observers as the name of the page. - The router imports each screen with a prefix of its own, so the names of screens never clash.
- The app opens on its start route, and
/redirects to it. When no route can start the app, as in an app without features,/shows the fallback start screen.
Screens get the values of their parameters from the location. An int or a double is parsed with tryParse, and a bool must be true or false exactly. An optional value that the location does not have, or that is not of its type, is null. A location without a valid required value, such as /catalog/item/abc for an int, shows the error screen of go_router.
The main navigation
When the app has a layout, such as bottom_tabs, and destinations, they form the main navigation: a StatefulShellRoute.indexedStack with a branch for each destination, in the order of the features, which shows the AppShell of the layout. The router gives the shell appDestinations of the layout role, and renders neither a label nor an icon itself.
- The routes below a destination stay in its branch, which keeps its stack while another branch is selected.
- The app opens on the branch of its start route.
- The other top-level routes are outside the main navigation, and the router matches the destinations first.
- Without destinations, or without a layout, there is no main navigation.
go_router keeps the pages of the branches, and the navigators that show them, with the route of the main navigation, for as long as a page of that route is in the widget tree (flutter/flutter#148768). A main navigation that came back sooner, such as in the same turn, would have the pages of its branches again. So the router keeps its routes in a RoutingConfig that go_router follows, and each time the main navigation leaves the pages of the router, it puts a new route for the main navigation there, which _mainNavigation() creates. The main navigation then comes back with each branch on its destination, also in the same turn or while the transition to the page that took its place is on its way.
Navigation
context.nav navigates through this router from any context, even one above the router.
go()shows the location with the chain of its parents below it, in the branch of the location when it is in the main navigation.push()shows the location on top and completes with the value its page returns. In the main navigation it goes on top of the stack of the selected branch, whichever branch the location belongs to, as go_router does.replace()replaces the top of the stack with the location, as go_router'spushReplacementdoes.
go_router shows the main navigation only once, so a location in it can only go on top of the main navigation itself. On a page shown over the main navigation, push() and replace() of such a location throw a StateError that says to use go(), and leave the stack as it is. On a page with no main navigation below it, push() brings the main navigation back on top with the location. See Navigation.
Without a page of a route
go_router has no page of a route in two states: before it shows its first location, and while it shows its error screen in place of the whole stack, for a location that no route matches or that the redirect of a route refuses. It then has no page to push over and none to replace. GoRouter.push would keep the error screen and never complete, or throw for a location in the main navigation, and GoRouter.pushReplacement throws for want of a page.
So in both states, push() and replace() of the router give go_router their location as go() does. The location shows with the pages of its chain, in place of the error screen, and such a push() completes with null. An error screen that a push showed over a page is a page of go_router, which pushes over it and replaces it like any other.
The back button of the system
The router closes the route on top when the user presses the back button of the system, as the router role asks. With no route to close, it leaves the button to the system, which closes the app.
go_router shows its error screen in place of the whole stack, for a location that no route matches or that the redirect of a route refuses, and then has no page of a route. Asked to close the route on top, its delegate throws a StateError there (flutter/flutter#187616). So the _GoRouter of the module has a dispatcher of the button of its own, which asks the root navigator while go_router has no page:
- A route that is shown over the error screen, such as a dialog, closes.
- With no route to close, the button is left to the system, as on the first page of the app.
- A
BackButtonListenerbelow the router hears of the button first there too. - An error screen that a push showed over a page is a page of go_router, which closes it.
Guards
A module can keep the user from the rest of the app until a condition holds, with a guard, or only from the routes that ask for a condition. In an app whose modules declare guards, the router asks them through the GuardedNavigation that the router role generates, which keeps the location that the user comes back to. The router remembers nothing of the gates itself, and it knows a location by its URI.
It asks at three places:
go(),push()andreplace()ask once, before they hand a location to go_router, with the routes of the pages that the user can get back to.- The top-level
redirectof theGoRouterasks about the locations that go_router parses on its own, with no pages: the one the app starts on, those of the platform, and that of a branch of the main navigation that the user selects. - The router listens to
guardChangesof the role. When a guard starts or stops allowing, it tells the role of the pages that pushes showed, the one on top first, and of the location below them. A page thatreplace()showed over other pages counts as one that a push showed, as it is one to go_router.
What it does with the answers:
| The role answers | What the router does |
|---|---|
A location in place of the stack: the target of a gate, the location that the user comes back to, or / for a flow that is over. | It goes to the location, and a push() that asked completes with null. The target of a gate is outside the main navigation, so the router gives go_router a new main navigation, and no branch keeps a page. A go() to / leaves the branches that are not selected as they are. |
| The target of a guard that stands for a condition, to open over the page on top. | It pushes the target and keeps the request waiting: how to make it again and, for a push(), the future that it returned. It keeps one request, and drops it once no page of the flow is among its pages, as when the user went back. |
| Pages to close: those of a flow that is over, or of routes whose condition stopped holding. | It takes them out of the pages of go_router with one restore(), and completes their pushes with null once the frame is over. Then it makes the request that waited again, or drops it when the role says so. |
A redirect can only send go_router to another location. So when the role answers a location from the platform with a page to open over the screen that the app starts on, the target of such a guard or a page of its flow that a link asked for, the router sends go_router to / and pushes that page once the parse is over. Two links that arrive in one turn open it once, for the later one. A second link to the page that is on top keeps that page, with what the user typed there: the router puts it back over / and pushes no new one.
The redirect of go_router runs within each call of go_router, also within those that the router makes itself, and it asks with no pages, as for a link. The answer of the role depends on the pages, so the router counts its own calls, and the redirect asks nothing within them:
- A call is the router's own when it hands go_router a location that it asked about itself, or that the role answered, or pages that are shown already. A refresh of go_router is one, since it parses the location below the pushed pages again.
- The push of the target of a guard is one too. If the
redirectwere asked about it, the role would answer to open the target again, without end. The router throws aStateErrorthen, and does not hang the app.
While go_router has no page of a route, the router asks the role nothing itself. go(), push() and replace() give go_router the location as go() does, and the redirect asks about it. So the flow of a guard opens over / there, in place of the error screen too.
The router does not hand guardChanges to go_router as its refreshListenable. A refresh asks only about the location below the pushed pages, and gives each of those pages a new completer.
In an app without guards, app_router_factory.dart has none of this code. Ask the guards in a router shows the code.
Observers and listeners of the screen
Every navigator, the root one and the one of each branch, creates observers of its own from the factories that modules give the router role. The branches do not notify the observers of the root navigator, so each page is reported once. For an observer, switching branches is not a navigation event.
To follow the screen the user sees, modules such as firebase_analytics give the router role listeners of the screen instead. The router listens to its go_router delegate and calls each listener once whenever the page on top of the app changes or shows another location, including on a switch to another branch, as the router role promises. The listener gets the full name of the route and the location, such as home.home and /home. For a screen that is not a route of a module, the route is null. That is the fallback screen, at /, and the error screen of go_router, at the location it could not show. The router calls each listener on its own, so a listener that throws keeps no other from hearing of the screen, and what it throws reaches neither the router nor the handlers of the errors of the app. In debug mode the router prints it.
The listeners hear only of pages that the user saw. When the router closes the pages of a flow and the request that waited shows another page over the page below them, they hear of that other page and not of the page below.
Options
--start=<path> sets the full path of the route the app starts on. It can name any route without required parameters that is outside the flow of a guard and asks for no condition, and a run without a terminal needs it when several routes can start the app. See the start screen.
Choosing it
smf create asks which module provides the router, with None as one of the answers. When a module you chose needs a router and go_router is the only module that provides one, smf create adds it by itself instead. To choose it without the question:
smf create my_app -m go_router
Version
The module uses go_router 17, which works with the Material library of Flutter 3.44. go_router 18 looks for the MaterialApp of the separate material_ui package to choose Material pages, and apps on Flutter 3.44 do not use that package.