Skip to main content

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 guardWhat it is
nameThe name of the guard in its module. The full name is <module id>.<name>, such as terms.accepted.
allowsA function of the app that returns a ValueListenable<bool>: whether the guard allows, which notifies its listeners when that changes.
redirectToThe 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 a push() completes with null at 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 happenedWhat the role remembersWhere 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:

routeGuardsThe 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.
guardChangesA Listenable that notifies when a guard starts or stops allowing.
GuardedNavigationThe 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​

RuleWhat it requires
router.guardsThe 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_functionsThe function of every guard is a top-level function of its file that takes no arguments and returns a ValueListenable<bool>.
router.guards_askedIn 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​