Skip to main content

Conditions of routes

A gate keeps the user from the whole app. Some screens need more than the rest of the app does: in an app that lets everyone in, the screen of the account is still only for a user with an account. A condition is what such a screen needs.

Three parties meet through it, and the modules among them need not know each other:

  • A role publishes the condition, since the role knows what it is. The auth role publishes AuthRole.account.
  • A feature asks for the condition on a route. It lists the role among its roles, and knows no module with a guard.
  • A module that requires or provides the role stands for the condition with a guard. The function of the guard says whether the condition holds, and its target is the route to show until it does.

The built-in sign_in does the last two itself: its route of the account asks for AuthRole.account, and its guard sign_in.hasAccount stands for it. A feature of yours that asks for the same condition gets the same guard, without naming sign_in.

Who declares what​

A role publishes a condition as a constant of its class, a RouteCondition with the role and a name:

static const account = RouteCondition(authRole, 'account');

Messages name a condition <role id>.<name>, such as auth.account. No code of an app has the name.

A feature asks for the condition in Route.conditions, and the routes below that route ask for it too. This is the route of the account of sign_in:

              Route(
'/account',
name: 'account',
screen: ScreenRef(
'AccountScreen',
import: ImportRef.app('$_folder/account_screen.dart'),
),
conditions: [AuthRole.account],
),

Every user gets to the main navigation, to the screen that the app starts on and to the flow of a guard. So a route that asks for a condition is none of these: it is no destination and below none, it is no start candidate and above none, and it is outside the flows of the guards of its module.

A module stands for the condition with a RouteGuard whose condition names it, as sign_in does:

              RouteGuard(
name: accountGuard,
allows: FunctionRef('signInHasAccount', import: _guards),
redirectTo: _route,
stage: GuardStage.identity,
resumes: false,
condition: AuthRole.account,
),

The module declares at most one guard for a condition, and an app has one: two modules with a guard for the same condition cannot be in one app.

What the user sees​

While the condition does not hold, a request for a route that asks for it opens the target of the guard over the page that the user is on, and the request waits. Here a user without an account is on the settings screen and taps the entry of the account, which pushes /sign_in/account:

What happensWhat the router does
The user goes back from the target.The user is on the page they were on, and the request is dropped. A push() that waited completes with null.
The condition holds.The router closes the pages of the flow and does what was asked. push() shows the route over the page that the user was on and completes with the value of its page. go() shows the route in place of the stack, and replace() in place of that page.
A further request for a route that asks for the condition, while a page of the flow is open.Nothing. The flow is open already, and the first request waits.
The platform asks for such a route, or code does before the app has a page.The target opens over the screen that the app starts on. Once the condition holds, the route shows as go() to it does.

For a route that asks for two conditions, the target of the first guard that does not allow opens. Once that guard allows, the router closes its flow and asks about the request again, so the target of the second guard opens in turn.

When the condition stops holding​

When the condition stops holding, the router closes each page that asks for it, with the pages over it. The user is then on the page that those pages were opened from. A page that asks for the condition and has no page below it, as after go() to it, leaves for the screen that the app starts on.

So a screen that ends the condition itself, as the screen of the account does with its sign-out, only ends it. It neither closes itself nor navigates; see what a screen does.

The flow of the guard​

The flow of such a guard is its target and the routes below it, as for a gate, and it is over once the guard allows. Until then every route shows that does not ask for the condition, and the routes of the flow are among them. So a module may navigate to the target of its guard while the condition does not hold, as to the sign-in that a guest opens.

How a page of the flow is asked forWhat the router shows
With push(), on a pageThe page of the flow over that page, so the user can go back from it. Once the condition holds, the router closes it as it closes a target that it opened itself.
With go(), on a pageThe page of the flow in place of the stack, as the code asked, with no way back.
With no pages, as by a linkThe page of the flow over the screen that the app starts on, with that screen alone below it. Back leads into the app, and the router closes the page once the condition holds. That goes for a route below the target too.

Asked for by a link, a page of the flow would stand alone otherwise, with no way back into the app. A flow that a gate keeps open too is the gate's, and its page takes the place of the stack.

The screens of the flow move between its routes with push() or replace(), which keep the request waiting. go() takes the place of the page that the flow was opened over, and the request goes with it.

Next to a gate​

The app asks its gates first, so a gate that does not allow decides for every route, and for the routes that ask for a condition too.

A guard that stands for a condition may show the target of a gate of its module. The two then have one flow, which is over only once both allow. The two guards of sign_in are such a pair: in an app whose gate lets everyone in, the screens of the sign-in still show while sign_in.hasAccount does not allow. The functions of two such guards read one notifier, so that the router never finds that one of them changed and the other did not yet.

When a gate stops allowing while the flow of a condition is open, the target of the gate takes the place of the stack, and the request is dropped with the pages of its flow. If that target is a page of the same flow, the request waits on and is dropped once the target leaves.

The router remembers a location only for a gate. A guard that stands for a condition takes the user from no location: when it stops allowing, the user is on the page below the pages that closed, whatever its resumes says. With resumes: false, such a guard still makes the router forget what a gate made it remember.

Two known limits​

Behind a gate that does not allow, the router remembers a location for the gate and opens no flow. Two kinds of links are lost that way:

  • A link to a route that asks for a condition. If the gate then allows while the condition does not hold, the user comes to the screen that the app starts on. The same link opens the target of the guard over the start screen in an app whose gates allow.
  • A link to a page of the flow of a guard that stands for a condition, behind a gate with another flow. The router never remembers a location in a flow.

So in an app that everyone may use, a link that opens the app on a first launch, behind an onboarding, to the screen of the account or to the sign-in leaves the user on the screen that the app starts on once the onboarding is finished.

An app without a guard for the condition​

An app may have no guard that stands for a condition: it lacks the role, or it has no module with such a guard. The routes that ask for the condition then show like any other, and the router role reports nothing.

Whether such an app is worth a warning is up to the role of the condition. Its hooks see the routes and the guards of the app when it requires or uses the router role, with RouterFacade.routesAsking(condition) and RouterFacade.guardFor(condition). The auth role warns this way when routes ask for AuthRole.account and no guard of the app stands for it, as in an app with a feature that asks for it and without sign_in. The warning names each such route, here the route orders.orders of such a feature:

The route orders.orders is for users with an account (it asks for the condition auth.account), but no module of the app has a guard for that condition, so every user gets to it.

It is a warning and no error, since the developer of the app may write the guard later.

In the app​

In the app, a guard that stands for a condition has routes, the full names of the routes that ask for it, which the role collects from every module. The app has nothing else of a condition. This is the guard of sign_in:

lib/core/router/app_router.dart
  RouteGuard(
'sign_in.hasAccount',
allows: guard0.signInHasAccount(),
redirectTo: const SignInSignInLocation(),
flow: const {'sign_in.signIn', 'sign_in.signUp', 'sign_in.resetPassword'},
resumes: false,
routes: const {'sign_in.account'},
),

The routes do not inherit in the app. The role lists each route below a route that asks, and a developer who adds a route by hand adds the routes below it too.

Rules​

RuleWhat it requires of a condition
router.routesA route asks only for conditions of the roles that its module requires, uses or provides. Such a route is outside the main navigation and the flows of the guards of the module, and neither it nor a route below it is a start candidate.
router.guardsA guard that stands for a condition is the only one of its module for it, and the module requires or provides the role of the condition.

smf create checks both before it generates an app. It also reports the guards of two modules that stand for one condition, and refuses a --start on a route that asks for a condition, also in an app without a guard for it.

Next​