Navigation / go_router
Flutter navigation moves between screens (routes). The classic Navigator push/pop API is fine for small apps; go_router is the recommended approach for production: declarative routes, deep linking, type-safe params, nested shells.
Navigator, go_router, params, deep links
EXAMPLE
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
// 1) Imperative Navigator API
class HomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Home')),
body: Center(
child: ElevatedButton(
onPressed: () {
Navigator.of(context).push(MaterialPageRoute(
builder: (_) => const DetailsPage(productId: 42),
));
},
child: const Text('Open details'),
),
),
);
}
}
class DetailsPage extends StatelessWidget {
final int productId;
const DetailsPage({super.key, required this.productId});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Product $productId')),
body: Center(
child: ElevatedButton(
onPressed: () => Navigator.of(context).pop('purchased'),
child: const Text('Buy'),
),
),
);
}
}
// 2) Receive the popped result
final result = await Navigator.of(context).push<String>(MaterialPageRoute(
builder: (_) => const DetailsPage(productId: 42),
));
if (result == 'purchased') showSnack(context, 'Thanks for buying!');
// 3) Named routes (small apps only)
MaterialApp(
initialRoute: '/',
routes: {
'/': (_) => const HomePage(),
'/details': (ctx) {
final args = ModalRoute.of(ctx)!.settings.arguments as Map;
return DetailsPage(productId: args['productId']);
},
},
);
Navigator.pushNamed(context, '/details', arguments: {'productId': 42});
// 4) go_router — the modern choice
// pubspec.yaml: go_router: ^14.0.0
final _router = GoRouter(
initialLocation: '/',
routes: [
GoRoute(path: '/', builder: (_, __) => const HomePage()),
GoRoute(
path: '/products/:id',
builder: (ctx, state) {
final id = int.parse(state.pathParameters['id']!);
return DetailsPage(productId: id);
},
routes: [
GoRoute(
path: 'reviews',
builder: (_, state) {
final id = int.parse(state.pathParameters['id']!);
return ReviewsPage(productId: id);
},
),
],
),
GoRoute(
path: '/search',
builder: (_, state) => SearchPage(q: state.uri.queryParameters['q']),
),
],
errorBuilder: (_, state) => NotFoundPage(uri: state.uri),
);
class App extends StatelessWidget {
@override
Widget build(BuildContext context) =>
MaterialApp.router(routerConfig: _router);
}
// Navigate by URL
context.go('/products/42'); // replace stack
context.push('/products/42'); // push on stack
context.go('/products/42/reviews');
context.goNamed('product', pathParameters: {'id': '42'});
context.go('/search?q=cameras');
// 5) Bottom tab shell (preserve state per tab)
final _router2 = GoRouter(
initialLocation: '/feed',
routes: [
StatefulShellRoute.indexedStack(
builder: (context, state, navigationShell) =>
ScaffoldWithNav(navigationShell: navigationShell),
branches: [
StatefulShellBranch(routes: [
GoRoute(path: '/feed', builder: (_, __) => const FeedPage()),
GoRoute(path: '/feed/:id', builder: (_, s) => PostPage(id: s.pathParameters['id']!)),
]),
StatefulShellBranch(routes: [
GoRoute(path: '/search', builder: (_, __) => const SearchPage()),
]),
StatefulShellBranch(routes: [
GoRoute(path: '/me', builder: (_, __) => const ProfilePage()),
]),
],
),
],
);
class ScaffoldWithNav extends StatelessWidget {
final StatefulNavigationShell navigationShell;
const ScaffoldWithNav({super.key, required this.navigationShell});
@override
Widget build(BuildContext context) => Scaffold(
body: navigationShell,
bottomNavigationBar: NavigationBar(
selectedIndex: navigationShell.currentIndex,
onDestinationSelected: (i) => navigationShell.goBranch(i, initialLocation: i == navigationShell.currentIndex),
destinations: const [
NavigationDestination(icon: Icon(Icons.home), label: 'Feed'),
NavigationDestination(icon: Icon(Icons.search), label: 'Search'),
NavigationDestination(icon: Icon(Icons.person), label: 'Me'),
],
),
);
}
// 6) Redirects and auth guards
final _router3 = GoRouter(
refreshListenable: authState, // ChangeNotifier; router re-evaluates when notified
redirect: (context, state) {
final loggedIn = authState.isLoggedIn;
final goingToLogin = state.uri.path == '/login';
if (!loggedIn && !goingToLogin) return '/login?from=${state.uri}';
if (loggedIn && goingToLogin) return '/';
return null; // no redirect
},
routes: [ /* … */ ],
);
// 7) Modal sheets and dialogs — keep them OUT of the router
showModalBottomSheet(context: context, builder: (_) => const FilterSheet());
showDialog(context: context, builder: (_) => const ConfirmDialog());
// These don't change the URL or affect deep linking — they're 'overlays', not 'destinations'.
// 8) Deep links — same routes as web URLs
// iOS: associated domains in entitlements
// Android: intent-filters with autoVerify="true" in AndroidManifest.xml
// go_router parses incoming deep links and matches the route table —
// you get the right page for free.
// 9) Pop with results, prevent back-press
final r = await context.push<bool>('/confirm');
if (r == true) deleteAccount();
class ConfirmExit extends StatelessWidget {
@override
Widget build(BuildContext context) {
return PopScope(
canPop: false,
onPopInvoked: (didPop) async {
if (didPop) return;
final ok = await showDialog<bool>(
context: context,
builder: (_) => AlertDialog(
title: const Text('Exit?'),
actions: [
TextButton(onPressed: () => Navigator.pop(context, false), child: const Text('Stay')),
FilledButton(onPressed: () => Navigator.pop(context, true), child: const Text('Exit')),
],
),
);
if (ok == true && context.mounted) Navigator.of(context).pop();
},
child: const Scaffold(/* … */),
);
}
}
// 10) Common bugs
// • Calling Navigator after the widget is unmounted → use context.mounted check
// • go_router path with leading slash inside nested route → resolves from root, not parent
// • Forgetting MaterialApp.router with go_router → reverts to imperative behaviour
// • Pushing onto a removed StatefulShellRoute branch → use goBranch, not go
// • Modal route loses params on Hot Reload — restate from URL
// • Deep link opens app but lands on home — make sure routes accept query and path params, redirect logic doesn't lose 'from'
Why it matters
Use go_router for any app you’ll ship: declarative routes, free deep linking, and StatefulShellRoute.indexedStack for the bottom-tab pattern that keeps each tab’s scroll position and navigation stack alive. Keep dialogs and bottom sheets outside the router — they’re overlays, not destinations, and treating them as routes causes back-button confusion.
Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.
Example
Example
Navigator.push(
context,
MaterialPageRoute(builder: (_) => const ProfilePage()),
);
Navigator.pop(context);
// Or use go_router for declarative URLs.
Try it Yourself »
Exercise
Open a new page.
Navigator.
(context, MaterialPageRoute(builder: (_) => const Profile()));
Four letters.
Discussion
Loading…