Skip to main content

Ask the guards in a router

A module can keep the user from the rest of the app with a guard, which it declares as data of the router role. Guards of routes describes what the user then sees, and what the role remembers. This page is for a module that provides the router role: what it calls, and when.

The class of the role​

In an app with guards, the template of the role generates routeGuards, redirectOf(), guardChanges and the class GuardedNavigation in lib/core/router/app_router.dart. The class keeps what the guards make a router remember, so a router only asks it and shows what it answers:

  1. The router creates one GuardedNavigation<L>, where L is how it knows a location that it can show as go() does, such as a URI. It gives the class its location /, the screen that the app starts on, and a function from an AppLocation of the navigation to its own location.
  2. Before it shows a location, the router calls asked(route, location) with the full name of the route of the location, or null for a location that does not belong to a route of a module. It asks 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 it. When the answer is a location, the router shows it in place of the other, as go() to it does, and completes such a push() with null at once.
  3. Each time guardChanges notifies, the router calls changed(pages) with the pages that the user can get back to, the one on top first: those of its root navigator and of its selected branch. Each page comes with the full name of its route or null, with its location, and with whether push() showed it. A router that has no page yet calls it with no pages. When the answer is a location, the router shows it in place of its whole stack. When it is null, the router leaves everything as it is.

With go_router​

go_router knows a location by its URI. Its GoRouter asks in its top-level redirect, and push() asks before it hands the location to go_router:

lib/core/router/app_router_factory.dart
  final GuardedNavigation<String> _guards = GuardedNavigation(
start: '/',
locationOf: (location) => location.path,
);
lib/core/router/app_router_factory.dart
    redirect: (context, state) =>
_guards.asked(state.topRoute?.name, '${state.uri}')?.location,
lib/core/router/app_router_factory.dart
  
Future<T?> push<T extends Object?>(AppLocation location) {
final guarded = _guards.asked(location.routeName, location.path);
if (guarded != null) {
config.go(guarded.location);
return Future.value();
}
return config.push<T>(location.path);
}

An app without guards has none of these names in app_router.dart, so a provider renders this code only when routerRole.facadeOf(input).guards is not empty. go_router gives its brick the variable guards for it, as its render hook shows.

What is checked​

The contract harness checks that, in an app with guards, the files of the provider create a GuardedNavigation and read guardChanges. So a router that knows nothing of the guards cannot be in an app with a module that needs them.

Whether the router shows what the class answers, only a running app shows. SMF's CI checks that in running apps, with every provider of the router role that SMF has. It also runs the same tests on routers with a known bug, such as one whose replace() leaves the stack as it is, and each must fail on its bug. Contributing lists these tests.