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 GuardedMaterialPageRoutes 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);