In Angular, one can use the canActivate
for route guarding.
In Flutter, how would one go about it? Where is the guard placed? How do you guard routes?
I'm thinking along the lines of this:
- User logged in. Their token is stored in Shared Preference (the right way to store tokens? )
- User closed the app.
- User opens app again. As the application starts, it determines if user is logged in (perhaps a service that checks the storage for token), and then
- If logged in, load the homepage route
- If not logged in, load the login page
I was stumbling over this problem too and ended up using a FutureBuilder
for this. Have a look at my routes:
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.
I don't think there is a route guarding mechanism per se, but 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. Something like
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. I've included the relevant snippet below.
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,
);
}
}
If you don't want them to see the login page at all if they are logged in, use the first approach and you can control the splash screen that shows before runApp
has a UI by exploring this answer.
I came up with the following solution for a web project, which allows me to easily introduce guarded routes without having to worry that an unauthorized user is able to access sensitive information.
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,
);
}
}