Flutter navigation redirection with Riverpod and GoRouter

Flutter navigation redirection with Riverpod and GoRouter

Go router is the most popular package to handle navigation in Flutter and is now maintained by the Flutter team.
In this article, we will see how to use it with Riverpod to secure a route.
For example, we will see how to redirect a user to the login page if he is not authenticated.
But there are many other use cases, like redirecting to the premium page if he is not subscribed…

Using Redirect with Riverpod

Understanding the redirect function

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

The redirect function is called every time the router is updated.
It returns a string that is the new path of the router only if you want to change the path.
If you don’t want to redirect the user, you can return null.
The current path if provided by calling state.matchedLocation.

Redirecting example with riverpod

In this example we will redirect the user to the premium page if he is not subscribed.
Here is the code:

// This is super important - otherwise, we would throw away the whole widget tree when the provider is updated.
final navigatorKey = GlobalKey<NavigatorState>();
// We need to have access to the previous location of the router. Otherwise, we would start from '/' on rebuild.
GoRouter? _previousRouter;

final routerProvider = Provider((ref) {
  final userState = ref.watch(userStateNotifierProvider);  return _previousRouter = GoRouter(
    initialLocation: _previousRouter?.routerDelegate.currentConfiguration.fullPath,
    navigatorKey: navigatorKey,
    routes: [...],
    redirect: (context, state) {
      try {
        final isLoading = userState!.subscription.maybeMap(
          loading: (_) => true,
          orElse: () => false,
        );
        if (isLoading) {
          return '/loading';
        }
        if (!userState.subscription.isActive && state.matchedLocation == '/') {
          debugPrint("Redirect to premium from (${state.matchedLocation})");
          return '/premium';
        }
      } catch (e) {
        debugPrint("Error in redirect: $e");
      }
      return null;
    },
  );
});

Note: this solution comes from the community (Thanks CreativeCreatororMaybeNot).
See this github issue here

This is a great solution

  • You can now be sure that the user will be redirected to the premium page if he is not subscribed.

  • You have one function to handle all the redirections.

The only problem with it is that it rebuilds the Router on any user state changes.
Imagine you updated the name of the user. You will rebuild the whole router.

On more complex app, having one function to handle all the redirections can be a problem. You may need many complex conditions to handle all the redirections.

Using a Guard for each route

The principle of a guard is to check conditions before displaying a route.
If the condition is not met, the user is redirected to another route.
This is a common pattern in web development.

example:

GoRoute(
    name: 'home',
    path: '/',
    builder: (context, state) => AuthGuard(
        fallbackRoute: '/signup',
        child: HomePage(),
    ),
),

In this example, the user will be redirected to the signup page if he is not authenticated.
This makes the conditions easy to read for each route.
And so you can protect each route separately.

The AuthGuard or Guard do not exist in the GoRouter package or Flutter.
We will have to create it ourselves.

Creating the Guard

We want to create a reusable widget that will check some conditions before displaying a route.
As the Flutter team says “Everything is a widget”.
So we will create a Guard widget.

class Guard extends StatelessWidget {
  final Future<bool> canActivate;
  final Widget child;
  final String fallbackRoute;

  const Guard({
    super.key,
    // this is the condition to check within a Future as it can be async
    required this.canActivate,
    // this is the child to display if the condition is met
    required this.child,
    // this is the route to redirect if the condition is not met
    required this.fallbackRoute,
  });  @override
  Widget build(BuildContext context) {
    return FutureBuilder(
      future: canActivate,
      builder: (_, result) {
        if (!result.hasData || result.hasError) {
          return Container();
        }
        final bool canActivate = result.data!;
        if (canActivate) {
          return child;
        }
        redirect(context);
        return Container();
      },
    );
  }  void redirect(BuildContext context) {
    WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
      context.pushReplacement(fallbackRoute);
    });
  }
}

The redirect function is called after the build of the widget.
This is important because you can’t navigate during the build.
So we use the addPostFrameCallback to navigate after the build.

Using the Guard to secure a route

Now we can use the Guard to secure a route.
Here is an example for securing the home route only for authenticated users.

class AuthGuard extends ConsumerWidget {
  final String fallbackRoute;
  final Widget child;  

  const AuthGuard({
    super.key,
    required this.fallbackRoute,
    required this.child,
  });  

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final authState = ref.watch(userStateNotifierProvider);
    return Guard(
      canActivate: authState.user.maybeMap(
        authenticated: (user) async => true,
        anonymous: (user) async => false,
      ),
      fallbackRoute: fallbackRoute,
      child: child,
    );
  }
}

I personnally prefer this solution because it is more readable (It’s what is included in ApparenceKit).
You can protect each route separately.
Moreover you don’t have to check the previous router state as in the previous solution.

I have a bit simplified the code of the Guard for the example.

To improve it, you can

  • add a loading state

  • add a transition state to display the redirection if needed

Conclusion

In this article, we have seen how to use the GoRouter package with Riverpod to secure a route.

We have seen two solutions:
👉 Using a redirect function to handle all the redirections
👉 Using a Guard for each route

Riverpod and GoRouter are two amazing packages that can be used together to create a powerful app.
I hope this article will help you to secure your app easily.