go_router

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.