5.8 KiB
go_router Guide
Use this reference when implementing, fixing, or reviewing go_router in a
Flutter app. Check the target app's installed go_router version before using a
newer API.
Basic Setup
Add the dependency to pubspec.yaml:
dependencies:
go_router: ^17.2.2
Minimal configuration:
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
final router = GoRouter(
routes: [
GoRoute(path: '/', builder: (context, state) => const HomeScreen()),
GoRoute(path: '/details', builder: (context, state) => const DetailsScreen()),
],
);
void main() {
runApp(MaterialApp.router(routerConfig: router));
}
Navigation APIs
Use URL locations for direct navigation:
context.go('/details'); // Replace the displayed route stack.
final result = await context.push<bool>('/picker'); // Push and wait for data.
if (!context.mounted) return;
Return a value:
context.pop(true);
Build URLs with query parameters using Uri:
final location = Uri(
path: '/search',
queryParameters: {'q': query, 'page': '$page'},
).toString();
context.go(location);
Do not call context.push('/search', queryParameters: {...}); push accepts a
location string and optional extra.
Named Routes
Named routes are useful when paths should be generated from route names and parameter maps.
GoRoute(
name: 'user',
path: '/users/:userId',
builder: (context, state) {
final userId = state.pathParameters['userId']!;
return UserScreen(userId: userId);
},
);
context.goNamed(
'user',
pathParameters: {'userId': '42'},
queryParameters: {'tab': 'activity'},
);
Use pushNamed when the destination should be added to the stack:
final saved = await context.pushNamed<bool>(
'editUser',
pathParameters: {'userId': '42'},
);
Reading Route Data
Path Parameters
Use path parameters for required addressable identity:
GoRoute(
path: '/projects/:projectId',
builder: (context, state) {
final projectId = state.pathParameters['projectId']!;
return ProjectScreen(projectId: projectId);
},
);
Query Parameters
Use query parameters for optional, shareable route state:
GoRoute(
path: '/search',
builder: (context, state) {
final query = state.uri.queryParameters['q'] ?? '';
final page = int.tryParse(state.uri.queryParameters['page'] ?? '1') ?? 1;
return SearchScreen(query: query, page: page);
},
);
Extra Data
Use extra only for data that does not need to survive refresh, restore,
sharing, or native deep-link entry.
context.push('/details', extra: item);
GoRoute(
path: '/details',
builder: (context, state) {
final item = state.extra as Item?;
return DetailsScreen(item: item);
},
);
On web, complex extra data can be dropped during browser serialization unless
the router is configured with a suitable codec. Prefer path/query parameters for
addressable state.
Shell Routes
Use ShellRoute for persistent UI around child routes:
ShellRoute(
builder: (context, state, child) {
return Scaffold(
body: child,
bottomNavigationBar: NavigationBar(
selectedIndex: switch (state.uri.path) {
final path when path.startsWith('/settings') => 1,
_ => 0,
},
destinations: const [
NavigationDestination(icon: Icon(Icons.home), label: 'Home'),
NavigationDestination(icon: Icon(Icons.settings), label: 'Settings'),
],
onDestinationSelected: (index) {
context.go(index == 0 ? '/home' : '/settings');
},
),
);
},
routes: [
GoRoute(path: '/home', builder: (context, state) => const HomeScreen()),
GoRoute(path: '/settings', builder: (context, state) => const SettingsScreen()),
],
);
Use StatefulShellRoute when each tab or branch needs its own independent
navigation stack. Keep branch selection derived from the current route instead
of duplicating selected-index state.
Redirects And Guards
Use redirects for login, onboarding, feature gates, or legacy URL migration. Keep redirects deterministic and loop-free.
GoRouter(
redirect: (context, state) {
final signedIn = AuthScope.of(context).isSignedIn;
final loggingIn = state.matchedLocation == '/login';
if (!signedIn && !loggingIn) {
final from = Uri.encodeComponent(state.uri.toString());
return '/login?from=$from';
}
if (signedIn && loggingIn) {
return state.uri.queryParameters['from'] ?? '/';
}
return null;
},
routes: [
// ...
],
);
For newer go_router versions, onEnter can block navigation before redirects.
Prefer it only when the installed version and app architecture justify that
extra control.
Error Handling
Add an explicit not-found or error surface for bad URLs:
GoRouter(
errorBuilder: (context, state) => NotFoundScreen(error: state.error),
routes: [
// ...
],
);
Validate path and query parameters inside route builders. If a parameter cannot be parsed, show a route error screen or redirect to a safe canonical location.
Common Pitfalls
- Mixing
Navigator.pushwith maingo_routerroutes makes browser and deep-link behavior harder to reason about. context.pushis imperative and can interact awkwardly with browser history. Usecontext.gofor canonical URL state.- Query params are read from
state.uri.queryParameters, notstate.queryParameters. context.goandcontext.pushtake a location string plus optionalextra; build query strings withUrior use named-route APIs.- Do not rely on
extrafor data that must survive browser refresh or native deep links. - Preserve intended destinations across auth redirects.
- Test shell routes for selected destination, back behavior, and independent
branch stacks when using
StatefulShellRoute.