DEV Community

Cover image for Guarding routes in Flutter with GoRouter and Riverpod
Dinko Marinac
Dinko Marinac

Posted on

Guarding routes in Flutter with GoRouter and Riverpod

Introduction

Guarding routes is a very common topic amongst mobile developers.

The problem can be boiled down to the following question:

"How can I have permission on the routes in my app?"

If you are using Flutter and Navigator 2.0 you can choose between the 3 most popular libraries for navigation: GoRouter, AutoRoute, and Beamer. AutoRouter and Beamer support Guards out of the box, but GoRouter does not. If you are using GoRouter, this article is for you.

The most common use case for route guarding is authentication. If the user is logged in, all functionality is available to him, if he is not, most or none is.

I will be using Riverpod as my preferred state management library, but you could use any other, the same principles apply.

GoRouter redirect

GoRouter exposes a redirect property that can be used to navigate the user on a different route. In the example of authentication guarding, the user would be redirected to the login page if he is not authenticated.

The redirect is defined as follows:

typedef GoRouterRedirect = FutureOr<String?> Function(BuildContext context, GoRouterState state);
Enter fullscreen mode Exit fullscreen mode

The callback supplies the BuildContext and GoRouterState and expects a nullable String as a result. If String is returned, it should be an existing route, and that means that a redirect will occur and the user will be redirected to the specified location. If null is returned, no redirect will occur.

Reacting to changes

The biggest issue is that the router must be specified when the app is created and there is no information about whether the user is logged in or not. Furthermore, this state can change during the runtime and the router should react accordingly. To solve this, the authentication state of the app should be observed.

Since I'm using Riverpod, this would translate to:

1. Create a provider that exposes the authentication state

2. Create a router provider

3. Watch the router provider in ProviderScope and supply the app with the router.

Let's say you have an auth service which exposes the authentication state as a nullable User object (like Firebase Authentication does). You can create a StreamProvider which exposes this state:

final currentUserProvider = StreamProvider<User?>((ref) {
  final auth = ref.watch(firebaseAuthProvider);
  return auth.authStateChanges();
}); 
Enter fullscreen mode Exit fullscreen mode

The next step is to create a routerProvider:

final routerProvider = Provider((ref) {
  return _routeConfig(redirect: (context, state) {
    final authState = ref.read(currentUserProvider);
    if (authState.isLoading || authState.hasError) return null;

    final isAuthenticated = authState.valueOrNull != null;
    final isAuthenticating = state.matchedLocation == Routes.login;

    if (!isAuthenticated) {
      return Routes.login;
    }

    if (isAuthenticating) {
      return Routes.dashboard;
    }

    return null;
  });
});

GoRouter _routeConfig({GoRouterRedirect? redirect = null}) => GoRouter(
      redirect: redirect,
      routes: [
          // routes..  
      ],  
     );  
Enter fullscreen mode Exit fullscreen mode

The _routeConfig method creates a router with a specified redirect. This is your router.

Redirect logic is as follows:

  • If the authentication state is loading or there is an error, don't perform the redirect.
  • If user is not authenticated, redirect him to login
  • If user is authenticated and on login, redirect to dashboard

The only thing left to do is to connect the router to MaterialApp:

class App extends StatelessWidget {
  const App({super.key});

  @override
  Widget build(BuildContext context) {
    return ProviderScope(
      observers: [ProviderLogger()],
      child: Consumer(
        builder: (_, ref, __) {
          final router = ref.watch(routerProvider);
          return MaterialApp.router(
            routerConfig: router,
            title: 'Flutter Demo',
            theme: AppTheme.lightTheme,
            darkTheme: AppTheme.darkTheme,
            themeMode: ThemeMode.dark,
            localizationsDelegates: [AppLocalizations.delegate],
          );
        },
      ),
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

Conclusion

That's it! If your user logs out or fails authentication for any reason, they will be redirected to the login page. Once they log in they can freely navigate within the app.

If you have found this short tutorial on how to implement route guarding in Flutter with GoRouter and Riverpod valuable, make sure to like and follow for more content like this.

Reposted from my blog.

Top comments (4)

Collapse
 
alienjob profile image
Alien_job

It doesn't work with logout - currentUserProvider doesn't watches. If I watch it - than it mess with initial location

Collapse
 
dinko7 profile image
Dinko Marinac

It works with logout, but it depends on how your router looks. This is not a one-size-fits-all solution. You should adapt it to your router.

Collapse
 
edmonbelchev profile image
Edmon Cvetomirov Belchev

Where is ProviderLogger() defined?

Collapse
 
dinko7 profile image
Dinko Marinac

In another file, it's irrelevant for this tutorial. If you need to run the code, delete the line.