skip to content

GoRouter Redirects & Params

go_router maps URL paths to GoRoute builders with path and query parameters, and a redirect re-run by refreshListenable guards auth. Interviewers ask when context.go differs from context.push.

part ofFlutteroverview, primer and where to startread it →
on this pageshow

explore

questions

6

With go_router, how do you redirect signed-out users of a banking app to a login route and send them back after sign-in?

level: middleimportance: must knowfreq 62%

answer

  1. one callback on the GoRouter itself
  2. return a location, or null to stay
  3. check matchedLocation before redirecting
  4. refreshListenable re-runs it on sign-in
  5. carry the origin in a from query

basics

~20 s

Give GoRouter a top-level redirect that returns '/login' for a signed-out user and null otherwise, and pass the session ChangeNotifier as refreshListenable so the redirect runs again the moment the user signs in or out.

solid answer

~40 s

go_router's top-level `redirect` has the signature `FutureOr<String?> Function(BuildContext, GoRouterState)` and runs on every navigation before any `GoRoute.redirect`. For the banking app it returns a login location when the session is signed out, and `null` (or the current location) to let the navigation through. I check `state.matchedLocation == '/login'` so the login page itself is exempt, encode the original location as `?from=` with the `Uri` class, and after sign-in send the user from `/login` back to `from`. The key extra is `refreshListenable: session`: a `Listenable` such as a `ChangeNotifier` that makes go_router re-parse the current location, so signing in or out re-runs the redirect without any screen calling `go`. The router is created once, outside `build`.

code

dart · 51 lines
dart
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';

class BankSession extends ChangeNotifier {
  bool _signedIn = false;
  bool get signedIn => _signedIn;

  void signIn() {
    _signedIn = true;
    notifyListeners();
  }

  void signOut() {
    _signedIn = false;
    notifyListeners();
  }
}

final BankSession session = BankSession();

final GoRouter router = GoRouter(
  initialLocation: '/accounts',
  refreshListenable: session,
  redirect: (BuildContext context, GoRouterState state) {
    final bool atLogin = state.matchedLocation == '/login';
    if (!session.signedIn) {
      if (atLogin) return null;
      return Uri(path: '/login', queryParameters: <String, String>{'from': state.uri.toString()})
          .toString();
    }
    if (atLogin) {
      final String? from = state.uri.queryParameters['from'];
      return (from != null && from.startsWith('/')) ? from : '/accounts';
    }
    return null;
  },
  routes: <RouteBase>[
    GoRoute(
      path: '/login',
      builder: (BuildContext context, GoRouterState state) =>
          TextButton(onPressed: session.signIn, child: const Text('Sign in')),
    ),
    GoRoute(
      path: '/accounts',
      builder: (BuildContext context, GoRouterState state) =>
          TextButton(onPressed: session.signOut, child: const Text('Sign out')),
    ),
  ],
);

void main() => runApp(MaterialApp.router(routerConfig: router));

go deeper

for a junior

Recall that a top-level redirect returns a location or null, and that refreshListenable makes go_router run it again when the session notifies.

for a middle

Explain the three branches of an auth redirect, why it checks matchedLocation, how the from parameter round-trips, and that top-level redirects run before route-level ones.

for a senior

Show you guard deep links and token expiry in the router rather than in screens, keep the redirect cheap, and validate the from parameter before following it.

for a principal

Discuss where access rules should live in a large app: one central redirect versus per-feature GoRoute redirects, and how to keep them from contradicting each other.

## Where the auth guard lives In go_router, access rules belong in the **router**, not in screens. A screen that checks the session in `initState` and calls `context.go('/login')` flashes protected content, misses deep links that land on another screen, and duplicates the rule everywhere. The router offers two redirect hooks: - **Top-level `redirect`** on the `GoRouter` constructor, called for every navigation before routes are processed. This is the natural home of a signed-in check. - **Route-level `GoRoute.redirect`**, called when a navigation is about to display that route, useful for rules that concern one subtree. Both have the type `GoRouterRedirect`, which is `FutureOr<String?> Function(BuildContext context, GoRouterState state)`. Returning a string moves the navigation to that location; returning `null`, or the location already being shown, leaves it alone. ## The banking-app redirect The rule has three branches: 1. **Signed out and not on `/login`**: send the user to `/login`, remembering where they were going in a `from` query parameter. 2. **Signed in and on `/login`**: the login page is no longer useful, so return `from` if present, otherwise `/accounts`. 3. **Anything else**: return `null`. Two details prevent classic bugs. First, the check uses `state.matchedLocation` (the matched path, without the query string), so `/login?from=...` still counts as the login page. Second, the `from` value is built with `Uri(path: '/login', queryParameters: {'from': state.uri.toString()})`, which encodes it; reading it back through `state.uri.queryParameters['from']` decodes it. ## refreshListenable: re-running the rule when the session changes A redirect runs when a navigation happens. Signing in is not a navigation: the user is still on `/login` when the session flips. `refreshListenable` closes that gap. It accepts any `Listenable`; go_router subscribes to it, and every notification makes the router re-parse the current location, which runs `redirect` again: - The session is a `ChangeNotifier` whose `signIn()` and `signOut()` call `notifyListeners()`. - After sign-in, the redirect sees a signed-in user on `/login` and returns `from`. - After sign-out, or when a token expires and the session notifies, a user deep inside `/accounts/42` is sent to `/login?from=/accounts/42` with no screen involved. A redirect that reads an `InheritedWidget` through `dependOnInheritedWidgetOfExactType` (how most `of(context)` lookups work) is also re-evaluated when that widget changes. A `Stream`-based auth source needs wrapping in a `Listenable`; go_router 5 removed its own stream adapter. ## What the user experiences A signed-out user taps a shared link to `/accounts/42` on a cold start: 1. The platform hands go_router the location `/accounts/42`; the redirect sees a signed-out session and returns a `/login` location whose `from` query parameter holds `/accounts/42`. 2. The redirect runs again on the new location; `matchedLocation` is `/login`, so it returns `null` and the login page is shown. 3. The user signs in; the session calls `notifyListeners()`. 4. go_router re-parses the current login location; the redirect now sees a signed-in user on `/login` and returns `/accounts/42`. 5. The account page appears, with the accounts list beneath it if the route is nested, exactly as if the user had navigated there. No screen contains navigation code for any of this; the login page only calls `signIn()`. ## Ordering and limits | Hook | When it runs | Typical use | |---|---|---| | `onEnter` (since go_router 16.3.0) | first, once per navigation, against the literal target | block a navigation outright | | top-level `redirect` | next, and again on each new location it produces | session guard | | `GoRoute.redirect` | for the matched routes | per-feature rules | Every location produced by redirects in one navigation is recorded. Producing the same location twice fails with a redirect-loop `GoException`, and more hops than `redirectLimit` (default **5**) fail with a too-many-redirects one; both end on the error screen. ## Practical rules - Create the `GoRouter` **once** (a top-level final, a field of a long-lived object, or a provider) and hand it to `MaterialApp.router(routerConfig: router)`. Building it inside `build` resets navigation to the initial location whenever that widget rebuilds. - Keep the redirect fast and side-effect free; it may run many times. It may be `async`, and go_router waits for the returned `Future`, but a slow check delays every navigation. - Treat `from` as untrusted input: accept it only when it starts with `/`, so a crafted link cannot send the user somewhere unexpected after sign-in.

  • With go_router, why must the GoRouter be created once instead of inside a build method?
    A new `GoRouter` is a new routing configuration and a new route information provider. Rebuilding the widget that creates it resets navigation to the initial location and throws away the current stack, so the router lives in a top-level final, a long-lived object, or a provider.
  • With go_router, in what order do the top-level redirect and GoRoute.redirect run?
    The top-level redirect runs first; if it produces a new location, it runs again on that location. Then route-level redirects run for the matched routes. All hops share one redirect history, so loops and the redirectLimit are enforced across both kinds.
  • With go_router, can a redirect wait for an asynchronous token check?
    Yes. `GoRouterRedirect` returns `FutureOr<String?>`, so the callback can be `async` and go_router waits for it. Because it runs on every navigation, keep it quick, for example by reading an already-loaded session rather than calling the network each time.

The redirect is a doorman who checks every visitor at the lobby; refreshListenable is the radio that tells him a resident's badge was just revoked, so he walks upstairs and escorts that resident out without waiting for them to use a door.

saying these in an interview costs you the question

  • Calls context.go('/login') from each protected screen's initState instead of a redirect.
  • Omits refreshListenable, so signing out leaves the user on the accounts page.
  • Believes the redirect runs only once, when the app starts.
  • Creates the GoRouter inside build, resetting navigation on every rebuild.
  • Believes refreshListenable only accepts a Stream of auth events.
open as a page

With go_router, what is the difference between context.go and context.push, and when would you use each?

level: middleimportance: must knowfreq 72%

basics

~20 s

context.go replaces the whole page stack with the pages the destination's route hierarchy defines and updates the URL; context.push adds the matched page on top of the current stack and returns a Future that completes with the value passed to pop.

open as a page

With go_router, how do you pass data to a route through path parameters, query parameters and extra, and read each one?

level: juniorimportance: should knowfreq 55%

basics

~10 s

Declare a :accountId segment in the GoRoute path and read state.pathParameters['accountId']; append ?month=2026-09 and read state.uri.queryParameters['month']; pass an object with extra: and read state.extra, which never appears in the URL.

open as a page

A go_router app shows its error screen with 'redirect loop detected' or 'too many redirects' after a sign-in change; how do you diagnose and fix it?

level: seniorimportance: should knowfreq 30%

basics

~20 s

go_router records each location a navigation's redirects produce: a repeated location throws 'redirect loop detected', and more hops than redirectLimit (default 5) throws 'too many redirects'. Turn on debugLogDiagnostics, find the guards that disagree, and give the chain a fixed point.

open as a page

With go_router_builder, what do typed routes built from TypedGoRoute and GoRouteData give you over string paths?

level: middleimportance: nice to knowfreq 24%

basics

~20 s

go_router_builder reads @TypedGoRoute annotations on GoRouteData subclasses and generates a mixin with location, go, push and replace plus a $appRoutes list, so path and query parameters become typed constructor fields and a missing or mistyped one is a compile-time error.

open as a page

With go_router 18, how does the top-level onEnter callback differ from redirect, and when would you choose it?

level: seniorimportance: nice to knowfreq 18%

basics

~20 s

onEnter runs once per navigation before any redirect, sees both the current and the next GoRouterState, and returns Allow or Block, so it can cancel a navigation and keep the user where they are; redirect only lets a navigation through or sends it elsewhere.

open as a page