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);
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();
});
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..
],
);
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],
);
},
),
);
}
}
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)
It doesn't work with logout - currentUserProvider doesn't watches. If I watch it - than it mess with initial location
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.
Where is ProviderLogger() defined?
In another file, it's irrelevant for this tutorial. If you need to run the code, delete the line.