skills/flutter-navigation/references/go_router-guide.md

245 lines
5.8 KiB
Markdown

# 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`:
```yaml
dependencies:
go_router: ^17.2.2
```
Minimal configuration:
```dart
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:
```dart
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:
```dart
context.pop(true);
```
Build URLs with query parameters using `Uri`:
```dart
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.
```dart
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:
```dart
final saved = await context.pushNamed<bool>(
'editUser',
pathParameters: {'userId': '42'},
);
```
## Reading Route Data
### Path Parameters
Use path parameters for required addressable identity:
```dart
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:
```dart
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.
```dart
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:
```dart
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.
```dart
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:
```dart
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
1. Mixing `Navigator.push` with main `go_router` routes makes browser and
deep-link behavior harder to reason about.
2. `context.push` is imperative and can interact awkwardly with browser history.
Use `context.go` for canonical URL state.
3. Query params are read from `state.uri.queryParameters`, not
`state.queryParameters`.
4. `context.go` and `context.push` take a location string plus optional `extra`;
build query strings with `Uri` or use named-route APIs.
5. Do not rely on `extra` for data that must survive browser refresh or native
deep links.
6. Preserve intended destinations across auth redirects.
7. Test shell routes for selected destination, back behavior, and independent
branch stacks when using `StatefulShellRoute`.