Redirection
Sometimes you want your app to redirect to a different location. go_router allows you to do this at a top level for each new navigation event or at the route level for a specific route.
Top-level redirection
Sometimes you want to guard pages from being accessed when they shouldn't be, e.g. when the user is not yet logged in. For example, assume you have a class that tracks the user's login info:
class LoginInfo extends ChangeNotifier {
var _userName = '';
String get userName => _userName;
bool get loggedIn => _userName.isNotEmpty;
void login(String userName) {
_userName = userName;
notifyListeners();
}
void logout() {
_userName = '';
notifyListeners();
}
}
You can use this info in the implementation of a redirect
function that you
pass as to the GoRouter
constructor:
class App extends StatelessWidget {
final loginInfo = LoginInfo();
...
late final _router = GoRouter(
routes: [
GoRoute(
path: '/',
builder: (context, state) => HomeScreen(families: Families.data),
),
...,
GoRoute(
path: '/login',
builder: (context, state) => LoginScreen(),
),
],
// redirect to the login page if the user is not logged in
redirect: (state) {
// if the user is not logged in, they need to login
final loggedIn = loginInfo.loggedIn;
final loggingIn = state.subloc == '/login';
if (!loggedIn) return loggingIn ? null : '/login';
// if the user is logged in but still on the login page, send them to
// the home page
if (loggingIn) return '/';
// no need to redirect at all
return null;
},
);
}
In this code, if the user is not logged in, we either let them continue to the
/login
location that they were already headed to by returning null
or we
redirect them to /login
by returning '/login'
.
On the other hand, if the user is already logged in but headed to /login
, we
redirect them to /
instead.
Finally, if the user is logged in and heading to somewhere besides /login
, we
just return null
, letting them continue on their merry way. The redirect
function will be called over and over until null
is returned to enable
multiple redirects.
To make it easy to access the login info wherever it's needed in the app, consider using a state management option like provider to put the login info into the widget tree:
class App extends StatelessWidget {
final loginInfo = LoginInfo();
// add the login info into the tree as app state that can change over time
@override
Widget build(BuildContext context) => ChangeNotifierProvider<LoginInfo>.value(
value: loginInfo,
child: MaterialApp.router(...),
);
...
}
With the login info in the widget tree, you can easily implement your login screen:
class LoginScreen extends StatelessWidget {
@override
Widget build(BuildContext context) => Scaffold(
appBar: AppBar(title: Text(_title(context))),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
ElevatedButton(
onPressed: () {
// log a user in, letting all the listeners know
context.read<LoginInfo>().login('test-user');
// go home
context.go('/');
},
child: const Text('Login'),
),
],
),
),
);
}
In this case, we've logged the user in and manually redirected them to the home
screen. That's because go_router doesn't know that the app's state has changed
in a way that affects the route. If you'd like to have the app's state cause
go_router to automatically redirect, you can use the refreshListenable
argument of the GoRouter
constructor:
class App extends StatelessWidget {
final loginInfo = LoginInfo();
...
late final _router = GoRouter(
routes: ...,
redirect: ...
// changes on the listenable will cause the router to refresh it's route
refreshListenable: loginInfo,
);
}
Since the loginInfo
is a ChangeNotifier
, it will notify listeners when it
changes. By passing it to the GoRouter
constructor, go_router will
automatically refresh the route when the login info changes. This allows you to
simplify the login logic in your app:
onPressed: () {
// log a user in, letting all the listeners know
context.read<LoginInfo>().login('test-user');
// router will automatically redirect from /login to / using
// refreshListenable
//context.go('/');
},
The use of the top-level redirect
and refreshListenable
together is
recommended because it will handle the routing automatically for you when the
app's data changes.
Refreshing with a Stream
If your route-driving state is not available as a Listenable
but instead as a
Stream
, you can wrap your stream with a GoRouterRefreshStream
. This makes it
possible for GoRouter
to react to a stream-based state managmenet solution
like flutter_bloc.
class App extends StatelessWidget {
final streamController = StreamController();
...
late final _router = GoRouter(
routes: ...,
redirect: ...
// changes on the listenable will cause the router to refresh it's route
refreshListenable: GoRouterRefreshStream(streamController.stream),
);
}
Route-level redirection
The top-level redirect handler passed to the GoRouter
constructor is handy
when you want a single function to be called whenever there's a new navigation
event and to make some decisions based on the app's current state. However, in
the case that you'd like to make a redirection decision for a specific route (or
sub-route), you can do so by passing a redirect
function to the GoRoute
constructor:
final _router = GoRouter(
routes: [
GoRoute(
path: '/',
redirect: (_) => '/family/${Families.data[0].id}',
),
GoRoute(
path: '/family/:fid',
builder: ...,
],
);
In this case, when the user navigates to /
, the redirect
function will be
called to redirect to the first family's page. Redirection will only occur on
the last sub-route matched, so you can't have to worry about redirecting in the
middle of a location being parsed when you're already on your way to another
page anyway.
Considerations
There are a couple of subtleties to consider when using route-level redirection.
If you have a route that has a redirect
function that only redirects under
certain conditions, you will still need to provide a builder
function, e.g.
GoRoute(
path: '/',
redirect: (_) => kGoElsewhere ? '/elsewhere' : null,
builder: (context, state) => ..., // need this if kGoElsewhere == false
)
The builder
function will be called when the redirect condition is false.
Also, if the redirect
function is part of a sub-route, you will need to
provide a builder
function, e.g.
GoRoute(
path: '/profile',
redirect: (_) => '/profile/home', // only called when going to /profile
builder: (c, s) => ..., // need this to build /profile/:section stack
routes: [
GoRoute(
path: ':section',
builder: ...ProfileScreen(state.params['section']!)...,
),
],
)
The redirect
function will only be called when it's the top-most route of a
stack of matched routes, so if the redirect
is provided on a route in the
middle of the stack, the builder
will be called when building pages for the
stack above.
In fact, you probably don't want to have a redirect
in the middle of your
stack of pages anyway. You probably want the redirect
function on a stack all
by itself, redirecting to another top-level route:
GoRoute(
path: '/profile',
redirect: (_) => '/profile/home', // builder not needed in this case
),
GoRoute
path: '/profile/:section',
builder: ...ProfileScreen(state.params['section']!)...,
)
Of course, in you don't have a route-level redirect
function at all in your
route, you will need a builder
function in that case as well.
Parameterized redirection
In some cases, a path is parameterized, and you'd like to redirect with those
parameters in mind. You can do that with the params
argument to the state
object passed to the redirect
function:
GoRoute(
path: '/author/:authorId',
redirect: (state) => '/authors/${state.params['authorId']}',
),
Multiple redirections
It's possible to redirect multiple times w/ a single navigation, e.g. / => /foo => /bar
. This is handy because it allows you to build up a list of routes over
time and not to worry so much about attempting to trim each of them to their
direct route. Furthermore, it's possible to redirect at the top level and at the
route level in any number of combinations.
If you redirect too many times, that's likely to indicate a bug in your app. By
default, more than 5 redirections will cause an exception. You can change this
by setting the redirectLimit
argument to the GoRouter
constructor.
The other trouble you need worry about is getting into a loop, e.g. / => /foo => /
. If that happens, you'll get an exception.
Example: Redirection and Query Parameters
Sometimes you're doing deep linking and you'd like a user to login first before going to the location that represents the deep link. In that case, you can use query parameters in the redirect function:
class App extends StatelessWidget {
final loginInfo = LoginInfo();
...
late final _router = GoRouter(
routes: ...,
// redirect to the login page if the user is not logged in
redirect: (state) {
// if the user is not logged in, they need to login
final loggedIn = loginInfo.loggedIn;
final loggingIn = state.subloc == '/login';
// bundle the location the user is coming from into a query parameter
final fromp = state.subloc == '/' ? '' : '?from=${state.subloc}';
if (!loggedIn) return loggingIn ? null : '/login$fromp';
// if the user is logged in, send them where they were going before (or
// home if they weren't going anywhere)
if (loggingIn) return state.queryParams['from'] ?? '/';
// no need to redirect at all
return null;
},
// changes on the listenable will cause the router to refresh it's route
refreshListenable: loginInfo,
);
}
In this example, if the user isn't logged in, they're redirected to /login
with a from
query parameter set to the deep link. When deciding on the value
of the from
parameter, the state
object has the location
and the subloc
to choose from. The location
includes any query parameters on the location
whereas the subloc
does not. For simplicity, we'll just use the subloc
.
However, since the query parameters will be URL encoded, you could use the
location
instead. Dealer's choice.
Once the user logs in, the router will be triggered to refresh the route,
resulting in a call to redirect
. In this case, since they're now logged in but
at the /login
location, they'll be sent to either the from
link in the query
parameters, if there is one, or to /
otherwise.
And that's it. The combination of the redirect
function and the
refreshListenable
means that the login page doesn't even have to worry about
the from
value -- it's just sent in the location during redirection and then
stripped off when it's no longer needed. Easy peasy lemon squeezy.
This same technique can be extended to include more than just the login page, as shown in the loading page example.