Guarding routes in Flutter with GoRouter and Riverpod
Tags: flutter, dart, mobile, programming
Published: 2024-02-26 09:38:44

## 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](https://pub.dev/packages/go_router), [AutoRoute](https://pub.dev/packages/auto_route), and [Beamer](https://pub.dev/packages/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:  
  
```dart
typedef GoRouterRedirect = FutureOr<String?> Function(BuildContext context, GoRouterState state);
```
  
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:  

```dart
final currentUserProvider = StreamProvider<User?>((ref) {
  final auth = ref.watch(firebaseAuthProvider);
  return auth.authStateChanges();
}); 
```
  
The next step is to create a `routerProvider`:  

```dart
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..  
      ],  
     );  
```

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:  
  
```dart
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],
          );
        },
      ),
    );
  }
}
```

## 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](https://dinkomarinac.dev/guarding-routes-in-flutter-with-gorouter-and-riverpod).