Routing
go_router 18 — shells, an auth guard, nested routes and path URLs.
Paths live in one place
lib/core/router/routes.dart holds every path as a constant on
R, plus the navigation model. The router, the sidebar and every
context.go read the same constants, so they cannot drift.
abstract final class R {
static const dashboard = '/';
static const orders = '/orders';
static const settings = '/settings';
…
}
The shell
Everything behind auth renders inside a ShellRoute, so the
sidebar and top bar survive navigation instead of rebuilding on every route.
ShellRoute(
builder: (context, state, child) =>
AppShell(location: state.matchedLocation, child: child),
routes: [
GoRoute(path: R.dashboard, builder: (_, _) => const DashboardScreen()),
…
],
)
The five auth screens and the 500 page are declared outside the shell, so they render standalone.
The guard
redirect: (context, state) {
final signedIn = ref.read(sessionProvider) != null;
final onAuth = R.isAuthRoute(state.matchedLocation);
if (!signedIn && !onAuth) return R.signIn;
if (signedIn && onAuth) return R.dashboard;
return null;
},
It runs on every navigation. Signing in or out re-evaluates it immediately
rather than on the next navigation, because a small ChangeNotifier
bridges the Riverpod session into refreshListenable.
Nested resource routes
Generated from one loop, so a fourth resource is a schema entry rather than four declarations:
for (final resource in const ['orders', 'customers', 'products'])
GoRoute(
path: '/$resource',
builder: (_, _) => ResourceListScreen(resource: resource),
routes: [
GoRoute(path: 'new', builder: …),
GoRoute(
path: ':id',
builder: (_, state) => ResourceDetailScreen(
resource: resource,
id: state.pathParameters['id']!,
),
routes: [GoRoute(path: 'edit', builder: …)],
),
],
),
new is declared before :id so it matches as
a literal rather than being captured as an id. Reversing them routes
/orders/new to the detail screen looking for a record called
"new".
Path URLs, not hash URLs
Flutter web defaults to /#/orders/10004. Kite calls
usePathUrlStrategy() in main.dart so the URL is
/orders/10004 — a page is only linkable and bookmarkable if the
URL looks like one.
The host must serve index.html for unknown paths, or
refreshing a detail page 404s. Cloudflare Pages, Netlify and Vercel do this
by default; tools/serve.mjs does it locally. See
Deploying.
The top bar title
Resolved by longest-prefix match against kNav, not equality — so
/orders/10004/edit still titles as Orders. Exact matching leaves
every nested route titled with the app name.
Navigating
context.go('/orders/10004'); // replace — the usual case in an admin
context.push('/orders/10004'); // stack — for a flow you back out of
Navigator.of(context).pop(); // dismiss a dialog or sheet