My solution is to build in a route guard system, much like the other libraries out there but where we can still use the original Navigator where needed, open a modal as a named route, chain guards, and add redirects. Its really basic but can be built on quite easily.
It seems like a lot but you'll just need 3 new files to maintain along with your new guards:
- router/guarded_material_page_route.dart
- router/route_guard.dart
- router/safe_navigator.dart
// Your guards go in here
- guards/auth_guard.dart
...
First create a new class that extends MaterialPageRoute
, or MaterialWithModalsPageRoute
if you're like me and want to open the Modal Bottom Sheet package. I've called mine GuardedMaterialPageRoute
class GuardedMaterialPageRoute extends MaterialWithModalsPageRoute {
final List<RouteGuard> routeGuards;
GuardedMaterialPageRoute({
// ScrollController is only needed if you're using the modals, as i am in this example.
@required Widget Function(BuildContext, [ScrollController]) builder,
RouteSettings settings,
this.routeGuards = const [],
}) : super(
builder: builder,
settings: settings,
);
}
Your route guards will look like this:
class RouteGuard {
final Future<bool> Function(BuildContext, Object) guard;
RouteGuard(this.guard);
Future<bool> canActivate(BuildContext context, Object arguments) async {
return guard(context, arguments);
}
}
You can now add GuardedMaterialPageRoute
s to your router file like so:
class Routes {
static Route<dynamic> generateRoute(RouteSettings settings) {
switch (settings.name) {
case homeRoute:
// These will still work with our new Navigator!
return MaterialPageRoute(
builder: (context) => HomeScreen(),
settings: RouteSettings(name: homeRoute),
);
case locationRoute:
// Following the same syntax, just with a routeGuards array now.
return GuardedMaterialPageRoute(
// Again, scrollController is only if you're opening a modal as a named route.
builder: (context, [scrollController]) {
final propertiesBloc = BlocProvider.of<PropertiesBloc>(context);
final String locationId = settings.arguments;
return BlocProvider(
create: (_) => LocationBloc(
locationId: locationId,
propertiesBloc: propertiesBloc,
),
child: LocationScreen(),
);
},
settings: RouteSettings(name: locationRoute),
routeGuards: [
// Now inject your guards, see below for what they look like.
AuthGuard(),
]
);
...
Create your async guard classes like so, as used above in our router.
class AuthGuard extends RouteGuard {
AuthGuard() : super((context, arguments) async {
final auth = Provider.of<AuthService>(context, listen: false);
const isAnonymous = await auth.isAnonymous();
return !isAnonymous;
});
}
Now you'll need a new class that handles your navigation. Here you check if you have access and simply run through each guard:
class SafeNavigator extends InheritedWidget {
static final navigatorKey = GlobalKey<NavigatorState>();
@override
bool updateShouldNotify(SafeNavigator oldWidget) {
return false;
}
static Future<bool> popAndPushNamed(
String routeName, {
Object arguments,
bool asModalBottomSheet = false,
}) async {
Navigator.of(navigatorKey.currentContext).pop();
return pushNamed(routeName, arguments: arguments, asModalBottomSheet: asModalBottomSheet);
}
static Future<bool> pushNamed(String routeName, {
Object arguments,
bool asModalBottomSheet = false,
}) async {
// Fetch the Route Page object
final settings = RouteSettings(name: routeName, arguments: arguments);
final route = Routes.generateRoute(settings);
// Check if we can activate it
final canActivate = await _canActivateRoute(route);
if (canActivate) {
// Only needed if you're using named routes as modals, under the hood the plugin still uses the Navigator and can be popped etc.
if (asModalBottomSheet) {
showCupertinoModalBottomSheet(
context: navigatorKey.currentContext,
builder: (context, scrollController) =>
(route as GuardedMaterialPageRoute)
.builder(context, scrollController));
} else {
Navigator.of(navigatorKey.currentContext).push(route);
}
}
return canActivate;
}
static Future<bool> _canActivateRoute(MaterialPageRoute route) async {
// Check if it is a Guarded route
if (route is GuardedMaterialPageRoute) {
// Check all guards on the route
for (int i = 0; i < route.routeGuards.length; i++) {
// Run the guard
final canActivate = await route.routeGuards[i]
.canActivate(navigatorKey.currentContext, route.settings.arguments);
if (!canActivate) {
return false;
}
}
}
return true;
}
}
To make it all work you will need to add the SafeNavigator key to your Material app:
MaterialApp(
navigatorKey: SafeNavigator.navigatorKey,
...
)
And now you can navigate to your routes and check if you have access to them like this:
// Opens a named route, either Guarded or not.
SafeNavigator.pushNamed(shortlistRoute);
// Opens a named route as a modal
SafeNavigator.pushNamed(shortlistRoute, asModalBottomSheet: true);
// Pops the current route and opens a named route as a modal
SafeNavigator.popAndPushNamed(shortlistRoute, asModalBottomSheet: true);