
Flutter Navigation
- 540 installs
- 105 repo stars
- Updated May 1, 2026
- madteacher/mad-agents-skills
flutter-navigation is a Claude Code skill that implements, fixes, refactors, reviews, migrates, and validates Flutter navigation and routing logic for developers who need reliable deep links, guards, and route state.
About
flutter-navigation is a version 2.0 agent skill from madteacher/mad-agents-skills that guides Flutter navigation implementation across Navigator, MaterialPageRoute, Flutter Router API, go_router, ShellRoute, StatefulShellRoute, nested Navigators, route guards, redirects, and deep-link platforms. The skill covers Android App Links, iOS Universal Links, custom URI schemes, Flutter web URL strategy, browser history, 404 routing, route tests, and navigation-state bug fixes. Developers reach for flutter-navigation when route data passing, shell layouts, or platform-specific link handling break production flows. It targets mobile engineers standardizing routing patterns before shipping cross-platform Flutter apps.
- Handles Navigator, MaterialPageRoute, Router API, go_router, ShellRoute, StatefulShellRoute and nested Navigators
- Manages route guards, redirects, deep links, Android App Links, iOS Universal Links and custom URI schemes
- Supports Flutter web URL strategy, browser history, 404 routing and navigation-state bug fixes
- Always inspects existing MaterialApp, Router, state-management, auth flows and tests before changes
- Verifies analyzer-clean Dart and correct route behavior after every navigation update
Flutter Navigation by the numbers
- 540 all-time installs (skills.sh)
- +4 installs in the week ending Aug 2, 2026 (Skillselion tracking)
- Ranked #603 of 2,245 Frontend Development skills by installs in the Skillselion catalog
- Data as of Aug 2, 2026 (Skillselion catalog sync)
npx skills add https://github.com/madteacher/mad-agents-skills --skill flutter-navigationAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 540 |
|---|---|
| repo stars | ★ 105 |
| Last updated | May 1, 2026 |
| Repository | madteacher/mad-agents-skills ↗ |
How do you fix Flutter go_router deep link routing?
Implement, fix, refactor, review, migrate or validate Flutter navigation and routing logic.
Who is it for?
Flutter developers debugging routing, migrating to go_router, or wiring Android App Links and iOS Universal Links.
Skip if: Teams building non-Flutter mobile stacks or backend-only services with no client routing layer.
When should I use this skill?
User mentions Flutter navigation, go_router, route guards, deep links, ShellRoute, or navigation-state bugs.
What you get
Working route tables, guarded redirects, deep-link handlers, nested Navigator layouts, and navigation unit tests.
- Route configuration
- Deep-link handlers
- Navigation tests
By the numbers
- Skill version 2.0 in madteacher/mad-agents-skills
- Covers go_router ShellRoute, StatefulShellRoute, and nested Navigator patterns
Files
Flutter Navigation
You are a Flutter navigation implementation agent. Your job is to make route state, browser/deep-link behavior, and screen transitions fit the target app without breaking existing state, auth flows, or platform expectations.
Principle 0
Navigation is user state. Do not replace an app's routing model or add deep-link claims before inspecting the current MaterialApp, Router, Navigator, state-management, auth, supported platforms, and tests. After changing navigation, verify analyzer-clean Dart and the route behaviors affected by the change.
Workflow
1. Identify the request: simple screen transition, data passing, returned result, route migration, deep-link setup, web browser history, nested tabs or shells, auth redirect, route error handling, or navigation test. 2. Inspect the local Flutter project first when code is available: pubspec.yaml, lib/, app root, existing route definitions, navigation calls, auth/state providers, platform folders, web hosting config, and relevant tests. 3. Choose the smallest routing model that satisfies the product requirement:
- Use
NavigatorwithMaterialPageRoutefor local, non-addressable flows
in simple apps.
- Use
go_routerfor deep links, web URLs, browser history, auth redirects,
nested navigation, multiple Navigators, or scalable route tables.
- Avoid new legacy
MaterialApp.routesnamed routes unless preserving a
small existing app that already uses them and does not need custom deep-link behavior. 4. Model route data deliberately. Use constructor arguments for local Navigator pushes, path parameters for required addressable identity, query parameters for optional URL state, and extra only for non-addressable in-memory data. 5. Implement in the app's existing style. Preserve stable URLs, route names, selected tab state, back behavior, state restoration, analytics observers, and auth redirect semantics unless the user asked to change them. 6. Add or update tests for the changed route behavior when feasible. Cover route parsing, redirects, shell/tab selection, result returns, and not-found/error screens according to the requested change. 7. Validate with the strongest local checks available. Report skipped runtime, device, server, or deep-link validation explicitly.
Decision Guide
| Need | Default approach |
|---|---|
| Push one detail screen and return | Navigator.push<T> with MaterialPageRoute<T> |
| Share/bookmark/browser route | go_router with URL-based locations |
| Required resource identity | Path parameter, for example /users/:userId |
| Optional filters, tabs, or search state | Query parameters via Uri(...).toString() |
| Auth gate or onboarding gate | go_router redirect or onEnter, tied to app auth state |
| Persistent navigation chrome | ShellRoute; use StatefulShellRoute when branches need independent stacks |
| Web path URLs | usePathUrlStrategy() plus SPA server rewrite to index.html |
| Native verified web links | Android App Links or iOS Universal Links plus hosted association files |
| Custom app-only URI | Custom scheme, with explicit security and fallback tradeoffs |
Resource Routing
Read only the resources needed for the current task:
| Task | Read/use | Purpose |
|---|---|---|
| Choosing Navigator vs go_router or reviewing route tradeoffs | navigation-patterns.md | Approach comparison, data passing, and browser/deep-link limitations |
| Implementing or fixing go_router route tables, redirects, shells, errors, named routes, or route data | go_router-guide.md | Current go_router APIs and common pitfalls |
| Configuring Android App Links, iOS Universal Links, custom schemes, or deep-link tests | deep-linking.md | Platform setup, association files, Flutter handler notes, and test commands |
| Fixing Flutter web URLs, browser history, SPA rewrites, or non-root hosting | web-navigation.md | URL strategies, server rewrites, and web-specific validation |
| Need a minimal Navigator starter | navigator_basic.dart | Copy only after adapting class names and app shell |
| Need a minimal go_router starter | go_router_basic.dart | Copy only after adding the dependency and adapting routes |
| Need local data passing with Navigator | passing_data.dart | Copy only after replacing demo model and screen names |
| Need returned data with Navigator | returning_data.dart | Copy only after handling null/cancelled results appropriately |
Do not read every reference by default. Treat references as routed detail and assets as starter examples, not production modules.
Implementation Rules
- Do not mix primary navigation models casually. If the app uses
go_router,
prefer context.go, context.push, context.goNamed, or context.pushNamed for main app routes. Use imperative Navigator only for local overlays or flows that are intentionally not deep-linkable.
- Build query-string locations with
Uri(path: ..., queryParameters: ...).toString()
or go_router named-route APIs. Do not pass a queryParameters argument to context.push or context.go.
- Read query parameters from
state.uri.queryParametersand path parameters
from state.pathParameters.
- Do not store complex state only in a URL. Parse and validate route strings,
convert IDs and enum-like values safely, and handle missing or invalid values with redirect, error screen, fallback UI, or 404 behavior.
- Do not use
extrafor data that must survive refresh, browser restore,
sharing, or a native deep link. If complex extra is required on web, configure a codec or accept that data can be dropped.
- Keep auth redirects loop-free. Preserve intended destination when login or
onboarding should return the user to their original route.
- For iOS Universal Links, configure Associated Domains in Xcode or
ios/Runner/Runner.entitlements, not as an Info.plist route table.
- For web path strategy, update
web/index.htmlbase href and server rewrites
when hosting below a non-root path. Do not encode the hosting prefix into every GoRoute.path unless the target app already uses that convention.
- Do not copy assets blindly. Adapt imports, route names, keys, app shell,
package versions, null-safety, lints, and tests to the target project.
Validation
After changing a Flutter project:
1. Run dart format on edited Dart files. 2. Run flutter analyze for the project or closest package. 3. Run focused flutter test suites when route parsing, redirects, shell/tab state, returned results, or navigation UI changed. 4. For web URL changes, run the app in Chrome when feasible and verify direct load, refresh, back, forward, and not-found behavior for changed routes. 5. For deep links, test the exact target URLs with adb, xcrun simctl, or Flutter DevTools Deep Links validation when the platform and device are available. 6. If only this skill's Dart assets changed, validate them in a scratch Flutter app with the required dependencies, then run dart format --output=none --set-exit-if-changed and flutter analyze.
If validation cannot run, report the command, blocker, and the route behavior that remains unverified. Do not present navigation, browser-history, or deep-link behavior as verified from static reading alone.
Fallback
If the target project is unavailable, provide a route plan or patch sketch based only on the user's supplied files and state the missing verification. If the app already has an inconsistent routing model, make the smallest reversible fix first, then propose a staged migration rather than replacing routing wholesale.
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
void main() {
runApp(
MaterialApp.router(
title: 'Navigation with go_router',
routerConfig: _router,
),
);
}
final _router = GoRouter(
routes: [
GoRoute(path: '/', builder: (context, state) => const HomeScreen()),
GoRoute(
path: '/details',
builder: (context, state) {
final id = state.uri.queryParameters['id'] ?? 'default';
return DetailsScreen(id: id);
},
),
],
);
class HomeScreen extends StatelessWidget {
const HomeScreen({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Home')),
body: Center(
child: ElevatedButton(
child: const Text('Go to details'),
onPressed: () {
final location = Uri(
path: '/details',
queryParameters: {'id': '123'},
).toString();
context.push(location);
},
),
),
);
}
}
class DetailsScreen extends StatelessWidget {
const DetailsScreen({super.key, required this.id});
final String id;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Details: $id')),
body: Center(
child: ElevatedButton(
child: const Text('Go back'),
onPressed: () => context.pop(),
),
),
);
}
}
import 'package:flutter/material.dart';
void main() {
runApp(const MaterialApp(title: 'Navigation basics', home: FirstScreen()));
}
class FirstScreen extends StatelessWidget {
const FirstScreen({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('First Screen')),
body: Center(
child: ElevatedButton(
child: const Text('Open second screen'),
onPressed: () {
Navigator.of(context).push(
MaterialPageRoute<void>(
builder: (context) => const SecondScreen(),
),
);
},
),
),
);
}
}
class SecondScreen extends StatelessWidget {
const SecondScreen({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Second Screen')),
body: Center(
child: ElevatedButton(
child: const Text('Pop current screen'),
onPressed: () {
Navigator.pop(context);
},
),
),
);
}
}
import 'package:flutter/material.dart';
class Todo {
final String title;
final String description;
const Todo(this.title, this.description);
}
void main() {
runApp(
MaterialApp(
title: 'Passing Data',
home: TodosScreen(
todos: List.generate(
20,
(i) => Todo(
'Todo $i',
'A description of what needs to be done for Todo $i',
),
),
),
),
);
}
class TodosScreen extends StatelessWidget {
const TodosScreen({super.key, required this.todos});
final List<Todo> todos;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Todos')),
body: ListView.builder(
itemCount: todos.length,
itemBuilder: (context, index) {
return ListTile(
title: Text(todos[index].title),
onTap: () {
Navigator.push(
context,
MaterialPageRoute<void>(
builder: (context) => DetailScreen(todo: todos[index]),
),
);
},
);
},
),
);
}
}
class DetailScreen extends StatelessWidget {
const DetailScreen({super.key, required this.todo});
final Todo todo;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text(todo.title)),
body: Padding(
padding: const EdgeInsets.all(16),
child: Text(todo.description),
),
);
}
}
import 'package:flutter/material.dart';
void main() {
runApp(const MaterialApp(title: 'Returning Data', home: HomeScreen()));
}
class HomeScreen extends StatelessWidget {
const HomeScreen({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Returning Data Demo')),
body: const Center(child: SelectionButton()),
);
}
}
class SelectionButton extends StatefulWidget {
const SelectionButton({super.key});
@override
State<SelectionButton> createState() => _SelectionButtonState();
}
class _SelectionButtonState extends State<SelectionButton> {
@override
Widget build(BuildContext context) {
return ElevatedButton(
onPressed: () {
_navigateAndDisplaySelection(context);
},
child: const Text('Pick an option, any option!'),
);
}
Future<void> _navigateAndDisplaySelection(BuildContext context) async {
final result = await Navigator.push<String>(
context,
MaterialPageRoute<String>(builder: (context) => const SelectionScreen()),
);
if (!context.mounted) return;
if (result == null) return;
ScaffoldMessenger.of(context)
..removeCurrentSnackBar()
..showSnackBar(SnackBar(content: Text(result)));
}
}
class SelectionScreen extends StatelessWidget {
const SelectionScreen({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Pick an option')),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Padding(
padding: const EdgeInsets.all(8),
child: ElevatedButton(
onPressed: () {
Navigator.pop(context, 'Yep!');
},
child: const Text('Yep!'),
),
),
Padding(
padding: const EdgeInsets.all(8),
child: ElevatedButton(
onPressed: () {
Navigator.pop(context, 'Nope.');
},
child: const Text('Nope.'),
),
),
],
),
),
);
}
}
Deep Linking Setup
Use this reference for Android App Links, iOS Universal Links, custom URI schemes, and deep-link validation. Prefer verified https links for production links that should open the app from the web.
Routing Model
Flutter can deliver incoming route information to Navigator or Router-based apps. For new deep-link work, prefer go_router or another Router-based solution so an incoming URL maps to a predictable page stack.
final router = GoRouter(
routes: [
GoRoute(path: '/', builder: (context, state) => const HomeScreen()),
GoRoute(
path: '/products/:productId',
builder: (context, state) {
final productId = state.pathParameters['productId']!;
return ProductScreen(productId: productId);
},
),
],
);If the app uses a third-party deep-link plugin, confirm whether Flutter's default deep-link handler should be disabled. In recent Flutter versions it is enabled by default; plugin-based handlers usually require opting out in platform configuration.
Android App Links
Android App Links use http or https links verified against a domain you own.
AndroidManifest.xml
Add an intent filter to the activity that should receive the link:
<activity
android:name=".MainActivity"
android:exported="true">
<intent-filter android:autoVerify="true">
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data
android:scheme="https"
android:host="example.com" />
</intent-filter>
</activity>For custom schemes, use a scheme such as myapp instead of https, but expect weaker verification and possible scheme conflicts.
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="myapp" />
</intent-filter>assetlinks.json
Host the association file at:
https://example.com/.well-known/assetlinks.jsonExample:
[
{
"relation": ["delegate_permission/common.handle_all_urls"],
"target": {
"namespace": "android_app",
"package_name": "com.example.app",
"sha256_cert_fingerprints": ["SHA256_FINGERPRINT"]
}
}
]Use the release signing fingerprint for production builds and include debug fingerprints only for debug validation.
Android Tests
adb shell am start -W -a android.intent.action.VIEW -d "https://example.com/products/42"
adb shell am start -W -a android.intent.action.VIEW -d "myapp://products/42"iOS Universal Links
iOS Universal Links use http or https and require Associated Domains. They are not configured by putting the Associated Domains entitlement in Info.plist.
Associated Domains
Add the Associated Domains capability in Xcode, or edit ios/Runner/Runner.entitlements:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.developer.associated-domains</key>
<array>
<string>applinks:example.com</string>
</array>
</dict>
</plist>Personal development teams might not support Associated Domains. Confirm the target signing team before treating Universal Links as verified.
apple-app-site-association
Host the association file with no .json extension:
https://example.com/.well-known/apple-app-site-associationExample:
{
"applinks": {
"apps": [],
"details": [
{
"appIDs": ["TEAMID.com.example.app"],
"paths": ["*"],
"components": [
{ "/": "/*" }
]
}
]
}
}Apple's CDN can delay Universal Link validation. Account for that delay when debugging fresh domain changes.
iOS Custom Schemes
Use Info.plist for custom schemes:
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleURLName</key>
<string>com.example.app</string>
<key>CFBundleURLSchemes</key>
<array>
<string>myapp</string>
</array>
</dict>
</array>Custom schemes are useful for app-only flows, but they are not domain-verified like Universal Links.
iOS Tests
xcrun simctl openurl booted "https://example.com/products/42"
xcrun simctl openurl booted "myapp://products/42"Web Deep Links
Flutter web handles URL paths in the browser. With the default hash strategy, links look like:
https://example.com/#/products/42With path strategy, links look like:
https://example.com/products/42Path strategy requires server rewrites to index.html. See web-navigation.md.
Validation Checklist
- Route table includes the expected incoming path.
- Required path parameters are parsed from
state.pathParameters. - Optional query values are parsed from
state.uri.queryParameters. - Invalid or missing values show an error screen, redirect, or safe fallback.
- Android manifest filters match scheme, host, and path expectations.
assetlinks.jsonuses the correct package name and signing fingerprint.- iOS Associated Domains are in Xcode capabilities or
Runner.entitlements. - The AASA file is reachable with the correct domain, team ID, and bundle ID.
- Third-party link plugins are not fighting Flutter's default handler.
- Device or simulator commands were run for the exact URLs changed.
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.2Minimal 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
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.
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/42Path strategy needs server rewrites but gives cleaner URLs:
https://example.com/details/42See web-navigation.md for setup and validation.
Web Navigation
Use this reference when fixing Flutter web URLs, browser history, refresh/direct load behavior, server rewrites, or non-root hosting.
URL Strategies
Flutter web supports two URL strategies:
Hash Strategy
https://example.com/#/path/to/screenUse it when you cannot configure the web server. Hash URLs avoid server rewrites because the route after # is not sent to the server.
Path Strategy
https://example.com/path/to/screenUse it when you control server rewrites and want clean, shareable paths.
import 'package:flutter_web_plugins/url_strategy.dart';
void main() {
usePathUrlStrategy();
runApp(const App());
}flutter_web_plugins is an SDK dependency:
dependencies:
flutter:
sdk: flutter
flutter_web_plugins:
sdk: flutterCall usePathUrlStrategy() before runApp().
Server Rewrites
Path strategy uses the browser History API. Configure the server to serve index.html for app routes that are not real files.
Nginx
location / {
try_files $uri $uri/ /index.html;
}Apache
<IfModule mod_rewrite.c>
RewriteEngine On
RewriteBase /
RewriteRule ^index\.html$ - [L]
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule . /index.html [L]
</IfModule>Firebase Hosting
{
"hosting": {
"public": "build/web",
"rewrites": [
{ "source": "**", "destination": "/index.html" }
]
}
}Vercel
{
"rewrites": [
{ "source": "/(.*)", "destination": "/index.html" }
]
}Netlify
/* /index.html 200Browser History
Router-based navigation, including go_router, integrates with the browser URL and History API for direct loads, refresh, back, and forward behavior.
Prefer canonical route state with context.go(location) for browser-visible navigation. context.push(location) is useful for page-stack style flows, but imperative navigation can be harder to reason about in browser history.
Build locations with query parameters using Uri:
context.go(
Uri(
path: '/search',
queryParameters: {'q': query},
).toString(),
);Hosting At A Non-Root Path
If the app is hosted at https://example.com/myapp/, update the base href in web/index.html:
<base href="/myapp/">Keep app route paths app-relative unless the target project already includes the deployment prefix in routes:
GoRouter(
routes: [
GoRoute(path: '/', builder: (context, state) => const HomeScreen()),
GoRoute(path: '/details/:id', builder: (context, state) => const DetailsScreen()),
],
);Configure the hosting server so /myapp/details/42 rewrites to the built index.html for that deployed app.
Not Found And Error Routes
For public web URLs, define a deliberate not-found or error surface:
GoRouter(
errorBuilder: (context, state) => NotFoundScreen(error: state.error),
routes: [
// ...
],
);Validate bad routes and malformed parameters. Do not let a bad URL crash during int.parse, forced casts, or missing path parameter access.
Web Validation
When feasible, run the app in Chrome and test:
- direct load of
/; - direct load of every changed route;
- refresh on every changed route;
- browser back and forward;
- query parameter preservation;
- unknown URL and malformed parameter behavior;
- deployed non-root path if applicable.
For production hosts, test after deployment as well as on the Flutter dev server. The dev server handles fallback routing automatically, while production hosting only works if rewrites are configured.
Accessibility And Titles
Navigation changes can affect focus, page announcements, and keyboard flow. After route changes, check that keyboard users can reach primary actions and that screen names, app bars, or semantic labels make the destination clear.
Related skills
How it compares
Pick flutter-navigation over generic mobile skills when the task is specifically Flutter route architecture, not general widget or state work.
FAQ
What Flutter routing APIs does flutter-navigation cover?
flutter-navigation covers Navigator, MaterialPageRoute, Flutter Router API, go_router, ShellRoute, StatefulShellRoute, nested Navigators, route guards, redirects, and platform deep-link handlers for Android and iOS.
When should developers use the flutter-navigation skill?
Developers should invoke flutter-navigation when implementing, refactoring, reviewing, migrating, or validating Flutter routing, including deep links, web URL strategy, 404 pages, and navigation-state test failures.