3.8 KiB
Navigation Patterns
Use this reference to choose a navigation approach before editing app code.
Approach Selection
Navigator With MaterialPageRoute
Use for simple, local flows that do not need to be addressable by URL:
- opening a detail screen from a list in a small mobile app;
- selecting a value and returning it to the previous screen;
- modal-like flows that should not survive browser refresh or external links.
Example: assets/navigator_basic.dart
final result = await Navigator.of(context).push<String>(
MaterialPageRoute<String>(
builder: (context) => const SelectionScreen(),
),
);
if (!context.mounted) return;
go_router
Use for route tables that need stable URLs or controlled page stacks:
- web apps with browser back, forward, refresh, and direct links;
- Android App Links, iOS Universal Links, or custom schemes;
- auth, onboarding, or feature redirects;
- nested navigation with persistent app shell;
- multiple Navigator branches;
- scalable route names and generated locations.
Example: assets/go_router_basic.dart
Legacy MaterialApp.routes Named Routes
Avoid adding new legacy named routes for most apps. They can handle simple static route names, but incoming deep links always push a new route and browser forward support is limited. Preserve them only when maintaining a small existing app with no custom deep-link or web-history requirements.
Data Passing
Constructor Data With Navigator
Use direct constructor arguments for local, in-memory data:
Navigator.push(
context,
MaterialPageRoute<void>(
builder: (context) => DetailScreen(item: item),
),
);
Example: assets/passing_data.dart
Path Parameters With go_router
Use path parameters for required identity:
GoRoute(
path: '/users/:userId',
builder: (context, state) {
final userId = state.pathParameters['userId']!;
return UserScreen(userId: userId);
},
);
context.go('/users/42');
Query Parameters With go_router
Use query parameters for optional URL state:
final location = Uri(
path: '/search',
queryParameters: {'q': 'flutter', 'tab': 'docs'},
).toString();
context.go(location);
Read them from state.uri.queryParameters:
final query = state.uri.queryParameters['q'] ?? '';
Extra Data With go_router
Use extra only for data that is intentionally not addressable:
context.push('/details', extra: item);
Do not rely on extra for browser refresh, shared URLs, or native deep-link
entry.
Returning Data
Navigator:
final result = await Navigator.push<String>(
context,
MaterialPageRoute<String>(builder: (context) => const SelectionScreen()),
);
if (!context.mounted) return;
go_router:
final result = await context.push<String>('/selection');
if (!context.mounted) return;
Return from the pushed route:
context.pop('selected-value');
Example: assets/returning_data.dart
Deep Linking Behavior
| Requirement | Prefer |
|---|---|
| Direct URL opens a predictable page stack | go_router or Router API |
| App replaces current pages when a link is opened | go_router or Router API |
| Browser forward/back support | go_router or Router API |
| Very simple mobile-only stack | Navigator |
| Static legacy app route names | Existing MaterialApp.routes, with limitations |
When Router and imperative Navigator are mixed, pages pushed imperatively are not deep-linkable and can be removed when the parent Router-backed page changes.
Web-Specific Choices
Hash strategy works without server changes:
https://example.com/#/details/42
Path strategy needs server rewrites but gives cleaner URLs:
https://example.com/details/42
See web-navigation.md for setup and validation.