Kite docs

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