How to Use Route Guard In Flutter

How to Use Route Guard In Flutter?

Guarded Routes without having to worry that an unauthorized user is able to access sensitive information. So in this article, we will learn about how to use Route Guard in flutter.

As mobile app developers, we understand the importance of ensuring smooth and secure navigation within our applications. Whether it’s restricting access to certain screens based on user authentication or implementing custom logic before allowing navigation, route guards play a vital role in enhancing the user experience and protecting sensitive information. In this tutorial, we’ll walk you through the concept of route guards in Flutter and demonstrate how you can effectively implement them in your own projects. Let’s dive in!

Check How to Use Route Guard In Flutter ??

Take a look at the code snippet.

final routes = {
  '/': (BuildContext context) => FutureBuilder<AuthState>(
    // This is my async call to sharedPrefs
    future: AuthProvider.of(context).authState$.skipWhile((_) => _ == null).first,
    builder: (BuildContext context, AsyncSnapshot<AuthState> snapshot) {
      switch(snapshot.connectionState) {
        case ConnectionState.done:
          // When the future is done I show either the LoginScreen 
          // or the requested Screen depending on AuthState
          return snapshot.data == AuthState.SIGNED_IN ? JobsScreen() : LoginScreen()
        default:
          // I return an empty Container as long as the Future is not resolved
          return Container();
      }
    },
  ),
};

If you want to reuse the code across multiple routes you could extend the FutureBuilder. you can do logic in the main function before loading the app or use the onGenerateRoute property of a MaterialApp.

One way to do that in your case is to await an asynchronous function that checks if the user is logged in before loading the initial route.

main() {
  fetchUser().then((user) {
    if (user != null) runApp(MyApp(page: 'home'));
    else runApp(MyApp(page: 'login'));
  });
}

But you may also be interested in the way the Shrine app does it.

They have the login page as the initial route in any case and remove it if the user is logged in.

That way the user sees the login page until it has been determined whether or not they log in.

class ShrineApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Shrine',
      home: HomePage(),
      initialRoute: '/login',
      onGenerateRoute: _getRoute,
    );
  }

  Route<dynamic> _getRoute(RouteSettings settings) {
    if (settings.name != '/login') {
      return null;
    }

    return MaterialPageRoute<void>(
      settings: settings,
      builder: (BuildContext context) => LoginPage(),
      fullscreenDialog: true,
    );
  }
}

The GuardedRoute class looks like this:

import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'package:kandabis_core/core.dart' as core;

Widget _defaultTransitionsBuilder(
    BuildContext context,
    Animation<double> animation,
    Animation<double> secondaryAnimation,
    Widget child) {

    return child;
}

class GuardedRoute extends PageRouteBuilder {

    GuardedRoute({
        @required final String guardedRoute,
        @required final String fallbackRoute,
        @required final Stream<bool> guard,
        @required final core.Router router,
        final RouteTransitionsBuilder transitionsBuilder = _defaultTransitionsBuilder,
        final bool maintainState = true,
        final Widget placeholderPage,
    })
    : super(
        transitionsBuilder: transitionsBuilder,
        maintainState: maintainState,
        pageBuilder: (context, animation, secondaryAnimation) =>

            StreamBuilder(
                stream: guard,
                builder: (context, snapshot) {

                    if (snapshot.hasData) {

                        // navigate to guarded route
                        if (snapshot.data == true) {

                            return router.routes[guardedRoute](context);
                        }

                        // navigate to fallback route
                        return router.routes[fallbackRoute](context);
                    }

                    // show a placeholder widget while the guard stream has no data yet
                    return placeholderPage ?? Container();
                }
            ),
    );
}
Using the guarded route is easy. You can define a guarded route and a fallback route (like a login page). Guard is a Stream which decides if the user can navigate to the guarded route. this is my Router class which shows how to use the GuardedRoute class:

class BackendRouter extends core.BackendRouter {

    BackendRouter(
        this._authenticationProvider,
        this._logger
        );

    static const _tag = "BackendRouter";

    core.Lazy<GlobalKey<NavigatorState>> _navigatorKey =

        core.Lazy(() => GlobalKey<NavigatorState>());

    final core.AuthenticationProvider _authenticationProvider;
    final core.Logger _logger;

    @override
    Map<String, WidgetBuilder> get routes => {

        core.BackendRoutes.main: (context) => MainPage(),
        core.BackendRoutes.login: (context) => LoginPage(),
        core.BackendRoutes.import: (context) => ImportPage(),
    };

    @override
    Route onGenerateRoute(RouteSettings settings) {

        if (settings.name == core.BackendRoutes.login) {

            return MaterialPageRoute(
                settings: settings,
                builder: routes[settings.name]
            );
        }

        return _guardedRoute(settings.name);
    }

    @override
    GlobalKey<NavigatorState> get navigatorKey => _navigatorKey();

    @override
    void navigateToLogin() {

        _logger.i(_tag, "navigateToLogin()");

        navigatorKey
            .currentState
            ?.pushNamed(core.BackendRoutes.login);
    }

    @override
    void navigateToImporter() {

        _logger.i(_tag, "navigateToImporter()");

        navigatorKey
            .currentState
            ?.pushReplacement(_guardedRoute(core.BackendRoutes.import));
    }

    GuardedRoute _guardedRoute(
         String route,
         {
             maintainState = true,
             fallbackRoute = core.BackendRoutes.login,
         }) =>

         GuardedRoute(
             guardedRoute: route,
             fallbackRoute: fallbackRoute,
             guard: _authenticationProvider.isLoggedIn(),
             router: this,
             maintainState: maintainState,
             placeholderPage: SplashPage(),
         );
}

And your application class looks like this:

class BackendApp extends StatelessWidget {

    @override
    Widget build(BuildContext context) {

        // get router via dependency injection
        final core.BackendRouter router = di.get<core.BackendRouter>();

        // create app
        return MaterialApp(
            onGenerateRoute: (settings) => router.onGenerateRoute(settings),
            navigatorKey: router.navigatorKey,
        );
    }
}

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

The GuardedRoute class looks like this:

import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'package:kandabis_core/core.dart' as core;

Widget _defaultTransitionsBuilder(
    BuildContext context,
    Animation<double> animation,
    Animation<double> secondaryAnimation,
    Widget child) {

    return child;
}

class GuardedRoute extends PageRouteBuilder {

    GuardedRoute({
        @required final String guardedRoute,
        @required final String fallbackRoute,
        @required final Stream<bool> guard,
        @required final core.Router router,
        final RouteTransitionsBuilder transitionsBuilder = _defaultTransitionsBuilder,
        final bool maintainState = true,
        final Widget placeholderPage,
    })
    : super(
        transitionsBuilder: transitionsBuilder,
        maintainState: maintainState,
        pageBuilder: (context, animation, secondaryAnimation) =>

            StreamBuilder(
                stream: guard,
                builder: (context, snapshot) {

                    if (snapshot.hasData) {

                        // navigate to guarded route
                        if (snapshot.data == true) {

                            return router.routes[guardedRoute](context);
                        }

                        // navigate to fallback route
                        return router.routes[fallbackRoute](context);
                    }

                    // show a placeholder widget while the guard stream has no data yet
                    return placeholderPage ?? Container();
                }
            ),
    );
}

Using the guarded route is easy. So you can define a guarded route and a fallback route (like a login page).

Guard is a Stream that decides if the user can navigate to the guarded route. This is my Router class which shows how to use the GuardedRoute class:

class BackendRouter extends core.BackendRouter {

    BackendRouter(
        this._authenticationProvider,
        this._logger
        );

    static const _tag = "BackendRouter";

    core.Lazy<GlobalKey<NavigatorState>> _navigatorKey =

        core.Lazy(() => GlobalKey<NavigatorState>());

    final core.AuthenticationProvider _authenticationProvider;
    final core.Logger _logger;

    @override
    Map<String, WidgetBuilder> get routes => {

        core.BackendRoutes.main: (context) => MainPage(),
        core.BackendRoutes.login: (context) => LoginPage(),
        core.BackendRoutes.import: (context) => ImportPage(),
    };

    @override
    Route onGenerateRoute(RouteSettings settings) {

        if (settings.name == core.BackendRoutes.login) {

            return MaterialPageRoute(
                settings: settings,
                builder: routes[settings.name]
            );
        }

        return _guardedRoute(settings.name);
    }

    @override
    GlobalKey<NavigatorState> get navigatorKey => _navigatorKey();

    @override
    void navigateToLogin() {

        _logger.i(_tag, "navigateToLogin()");

        navigatorKey
            .currentState
            ?.pushNamed(core.BackendRoutes.login);
    }

    @override
    void navigateToImporter() {

        _logger.i(_tag, "navigateToImporter()");

        navigatorKey
            .currentState
            ?.pushReplacement(_guardedRoute(core.BackendRoutes.import));
    }

    GuardedRoute _guardedRoute(
         String route,
         {
             maintainState = true,
             fallbackRoute = core.BackendRoutes.login,
         }) =>

         GuardedRoute(
             guardedRoute: route,
             fallbackRoute: fallbackRoute,
             guard: _authenticationProvider.isLoggedIn(),
             router: this,
             maintainState: maintainState,
             placeholderPage: SplashPage(),
         );
}

And your application class looks like this:

class BackendApp extends StatelessWidget {

    @override
    Widget build(BuildContext context) {

        // get router via dependency injection
        final core.BackendRouter router = di.get<core.BackendRouter>();

        // create app
        return MaterialApp(
            onGenerateRoute: (settings) => router.onGenerateRoute(settings),
            navigatorKey: router.navigatorKey,
        );
    }
}

So now, The GuardedRoute class looks like this:

import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'package:kandabis_core/core.dart' as core;

Widget _defaultTransitionsBuilder(
    BuildContext context,
    Animation<double> animation,
    Animation<double> secondaryAnimation,
    Widget child) {

    return child;
}

class GuardedRoute extends PageRouteBuilder {

    GuardedRoute({
        @required final String guardedRoute,
        @required final String fallbackRoute,
        @required final Stream<bool> guard,
        @required final core.Router router,
        final RouteTransitionsBuilder transitionsBuilder = _defaultTransitionsBuilder,
        final bool maintainState = true,
        final Widget placeholderPage,
    })
    : super(
        transitionsBuilder: transitionsBuilder,
        maintainState: maintainState,
        pageBuilder: (context, animation, secondaryAnimation) =>

            StreamBuilder(
                stream: guard,
                builder: (context, snapshot) {

                    if (snapshot.hasData) {

                        // navigate to guarded route
                        if (snapshot.data == true) {

                            return router.routes[guardedRoute](context);
                        }

                        // navigate to fallback route
                        return router.routes[fallbackRoute](context);
                    }

                    // show a placeholder widget while the guard stream has no data yet
                    return placeholderPage ?? Container();
                }
            ),
    );
}

Using the guarded route is easy. You can define a guarded route and a fallback route (like a login page).

Guard is a Stream that decides if the user can navigate to the guarded route. So this is my Router class which shows how to use the GuardedRoute class:

class BackendRouter extends core.BackendRouter {

    BackendRouter(
        this._authenticationProvider,
        this._logger
        );

    static const _tag = "BackendRouter";

    core.Lazy<GlobalKey<NavigatorState>> _navigatorKey =

        core.Lazy(() => GlobalKey<NavigatorState>());

    final core.AuthenticationProvider _authenticationProvider;
    final core.Logger _logger;

    @override
    Map<String, WidgetBuilder> get routes => {

        core.BackendRoutes.main: (context) => MainPage(),
        core.BackendRoutes.login: (context) => LoginPage(),
        core.BackendRoutes.import: (context) => ImportPage(),
    };

    @override
    Route onGenerateRoute(RouteSettings settings) {

        if (settings.name == core.BackendRoutes.login) {

            return MaterialPageRoute(
                settings: settings,
                builder: routes[settings.name]
            );
        }

        return _guardedRoute(settings.name);
    }

    @override
    GlobalKey<NavigatorState> get navigatorKey => _navigatorKey();

    @override
    void navigateToLogin() {

        _logger.i(_tag, "navigateToLogin()");

        navigatorKey
            .currentState
            ?.pushNamed(core.BackendRoutes.login);
    }

    @override
    void navigateToImporter() {

        _logger.i(_tag, "navigateToImporter()");

        navigatorKey
            .currentState
            ?.pushReplacement(_guardedRoute(core.BackendRoutes.import));
    }

    GuardedRoute _guardedRoute(
         String route,
         {
             maintainState = true,
             fallbackRoute = core.BackendRoutes.login,
         }) =>

         GuardedRoute(
             guardedRoute: route,
             fallbackRoute: fallbackRoute,
             guard: _authenticationProvider.isLoggedIn(),
             router: this,
             maintainState: maintainState,
             placeholderPage: SplashPage(),
         );
}
And your application class looks like this:

class BackendApp extends StatelessWidget {

    @override
    Widget build(BuildContext context) {

        // get router via dependency injection
        final core.BackendRouter router = di.get<core.BackendRouter>();

        // create app
        return MaterialApp(
            onGenerateRoute: (settings) => router.onGenerateRoute(settings),
            navigatorKey: router.navigatorKey,
        );
    }
}

Conclusion:

Thanks for being with us on a Flutter Journey !!!

Also, share this article with your friends and let them know how to use Route guard in flutter 🙂

Keep Learning !!! Keep Fluttering !!!

So in this article, we have been through how to use Route guard in flutter.

Drop us your valuable suggestion/feedback to serve you better.

FlutterAgency.com is our portal Platform dedicated to Flutter Technology and Flutter Developers. The portal is full of cool resources from Flutter like Flutter Widget GuideFlutter ProjectsCode libs and etc.

FlutterAgency.com is one of the most popular online portals dedicated to Flutter Technology and daily thousands of unique visitors come to this portal to enhance their knowledge of Flutter.

Abhishek Dhanani

Written by Abhishek Dhanani

Abhishek Dhanani, a skilled software developer with 3+ years of experience, masters Dart, JavaScript, TypeScript, and frameworks like Flutter and NodeJS. Proficient in MySQL, Firebase, and cloud platforms AWS and GCP, he delivers innovative digital solutions.

Leave a comment

Your email address will not be published. Required fields are marked *


Discuss Your Project

Connect with Flutter Agency's proficient skilled team for your app development projects across different technologies. We'd love to hear from you! Fill out the form below to discuss your project.

Have Project For Us

Get in Touch

"*" indicates required fields

ready to get started?

Fill out the form below and we will be in touch soon!

"*" indicates required fields