Declarative Routing
Why imperative push/pop breaks down
Section titled “Why imperative push/pop breaks down”Imperative navigation works fine until you need a deep link. When a user opens myapp.com/orders/42 from a cold start, there is no sequence of push calls that happened — you must reconstruct the whole stack (Home → Orders → Order 42) from a URL. Doing that by hand with Navigator.push is painful and error-prone.
The fix is to make the route stack a declarative function of state: given the current URL or app state, compute the stack. This is Navigator 2.0. Because the raw API is verbose, almost everyone uses go_router, the official package built on top of it.
flowchart LR state["app state / URL /orders/42"] --> router["go_router (route table)"] router --> stack["computed stack Home then Orders then Order 42"]
A go_router configuration
Section titled “A go_router configuration”You declare the routes once, as data. go_router maps a path to a builder, supports typed path parameters, and keeps the browser URL in sync automatically on web.
final router = GoRouter( routes: [ GoRoute(path: '/', builder: (context, state) => const HomeScreen()), GoRoute( path: '/orders/:id', // typed path parameter builder: (context, state) { final id = state.pathParameters['id']!; return OrderScreen(orderId: id); }, ), ],);
// Navigate by path — no manual stack juggling:context.go('/orders/42');context.go('/orders/42') sets the location; the router recomputes the stack. On web, the URL bar updates to match, and pasting that URL reconstructs the same screen — deep linking for free.
Nested / shell routes
Section titled “Nested / shell routes”A shell route wraps a set of routes in shared UI — the classic bottom-navigation app where the tab bar persists while the inner content changes. The shell stays mounted; only the child route swaps.
ShellRoute( builder: (context, state, child) => ScaffoldWithNavBar(child: child), routes: [ GoRoute(path: '/feed', builder: (context, state) => const FeedScreen()), GoRoute(path: '/profile', builder: (context, state) => const ProfileScreen()), ],);Redirects as auth guards
Section titled “Redirects as auth guards”A redirect runs before a route builds and can send the user elsewhere — the idiomatic place for authentication guards. Return a new path to redirect, or null to proceed.
GoRouter( redirect: (context, state) { final loggedIn = auth.isLoggedIn; final goingToLogin = state.matchedLocation == '/login'; if (!loggedIn && !goingToLogin) return '/login'; // guard if (loggedIn && goingToLogin) return '/'; // already in return null; // no redirect }, routes: [ /* ... */ ],);