Guards of routes
A guard keeps the user from the rest of the app until a condition holds, such as an onboarding that the user has to finish first, or terms to accept. The built-in example is onboarding. A module declares a guard as data of the router role, next to its routes. Whichever module provides the router then asks the guards of every module the same way, and the other modules of the app know nothing of them.
A gate over the whole app
A module declares a guard as a RouteGuard of smf_contracts:
| Part of a guard | What it is |
|---|---|
name | The name of the guard in its module. The full name is <module id>.<name>, such as terms.accepted. |
allows | A function of the app that returns a ValueListenable<bool>: whether the guard allows, which notifies its listeners when that changes. |
redirectTo | The target of the guard: a route of the module, which the router shows while the guard does not allow. |
The target and the routes below it are the flow of the guard, the screens that the user may see while the guard does not allow. The router shows the target in place of every location outside the flow, of whichever module.
That makes a guard a gate over the whole app. A condition that only some routes ask for, such as a paid screen, is not a guard, and the router role has nothing for it.
The target is a top-level route of the module that needs no values and is outside the main navigation, which the guard keeps the user out of. No route of the flow can start the app.
The app calls the function of a guard once, when its router first asks the guards, which is after bootstrap(). From then on it reads the value whenever it asks. So the value is known without waiting: a guard that depends on something saved on the device takes it in bootstrap(), as a restorer of the preferences does. The value changes outside the build of a frame, such as in the handler of a tap, because the router navigates when it changes.
What the user sees
A feature terms keeps the user at /terms until the terms are accepted. The app starts on /home:
The screen of the flow only changes what the guard reads. It navigates nowhere: the router leaves the flow.
What every router does
Before it shows a location, a router asks the guards about it. That holds for the location the app starts on, for each location that go(), push() or replace() is asked to show, and for each location that the platform gives the router. When a guard keeps the user from the location, the router:
- shows the target in its place, as
go()to the target does. The target takes the whole stack, and the stacks of every branch of the main navigation. Such apush()completes withnullat once; - never builds the screen of the other location, and the listeners of the screen never hear of it.
When a guard starts or stops allowing, the router tells the role of the pages that the user can get back to, and shows the location that the role answers in place of its whole stack. When the role answers none, the router leaves everything as it is.
With several guards, the app asks them in the order of the modules, and the guards of one module in the order it declares them. The first one that does not allow decides, and no later one is asked, which is why the flows of the guards show one after another.
Where the user comes back to
The role remembers one location for the user to come back to:
| What happened | What the role remembers | Where the user lands once the guards allow |
|---|---|---|
| The user or the platform asked for a location that a guard kept them from. | That location. A later one replaces an earlier one, so a link that arrives while the flow is shown wins. | The remembered location. |
| A guard stopped allowing while the user was in the app, and nothing was remembered yet. | The location below the pages that pushes showed, such as the tab that those pages were opened from. When pushes showed every page, the screen that the app starts on. | The remembered location. |
| A guard started allowing while a page of its flow was on top, and nothing was remembered. | Nothing. | The screen that the app starts on. |
The role never remembers a location in the flow of a guard: once that guard allows, its flow is over. Once the router has shown the remembered location, the role forgets it.
A module whose guard stops allowing by what its own screens do, such as a sign-out, decides whether the user comes back to where they were. If it goes to the target of the guard with go() before the guard stops allowing, the guard takes the user from no location, and the router shows the start screen once the guard allows again.
What the role generates
In an app whose modules declare guards, the template of the router role adds to lib/core/router/app_router.dart:
routeGuards | The guards of the app, in the order the app asks them. Each is a RouteGuard of the app, a class that the role generates into that file, with the name of the guard, its allows, its target and the names of the routes of its flow. It is not the class of smf_contracts with the same name, with which a module declares the guard. |
redirectOf(routeName) | The location that the guards show in place of a route, or null when they let the user see it. |
guardChanges | A Listenable that notifies when a guard starts or stops allowing. |
GuardedNavigation | The class through which a router asks the guards, with asked and changed. It keeps the remembered location. |
The memory of the guards is in the role, written once, and a module that provides the router only asks and shows the answer. So routers that tell the role of the same pages bring the user back to the same location, on the same change of a guard. Only whether a page that replace() showed counts as one that a push showed is up to the router, so after a replace() that location may differ between routers. An app without guards gets none of this code.
Rules
| Rule | What it requires |
|---|---|
router.guards | The guards of a module have valid names and functions of the app. Each shows a top-level route of the module that needs no values, is outside the main navigation, and has no start candidate in its flow. |
router.guard_functions | The function of every guard is a top-level function of its file that takes no arguments and returns a ValueListenable<bool>. |
router.guards_asked | In an app with guards, the files of the module that provides the router create a GuardedNavigation and read guardChanges. |
The first is checked before an app is generated, and the other two by the contract harness. The last one keeps a router that knows nothing of the guards out of an app with a module that needs them. It does not tell whether a router shows what the guards answer: only a running app shows that, and SMF's CI checks it there with every router that SMF has. See Contributing.
smf create also refuses a --start in the flow of a guard, since the app shows the flow until the guard allows, and then the screen that it starts on.
Next
- Navigation describes the guards from the side of the app.
- Write a feature module declares the guard of this page.
- Ask the guards in a router shows how a router asks the guards, and
go_routerhow go_router does.