Every multi-screen app needs navigation, and deep links and web URLs require declarative routing.
The `Navigator` manages a stack of routes (screens).
Navigator.push(context, MaterialPageRoute(builder: (_) => DetailScreen(id: 3)))shows a new screen; arguments are just constructor parameters.Navigator.pop(context, result)returns to the previous screen, optionally with a value.pushreturns aFuturethat completes with that result, sofinal picked = await Navigator.push(...)works for pickers.pushReplacementswaps the current screen (e.g. after login);pushAndRemoveUntilclears the stack.showDialogandshowModalBottomSheetalso push routes and return futures.
This imperative API is fine for simple apps. For deep links, web URLs, redirects (auth guards) and nested navigation such as bottom tabs that keep their own stacks, use a declarative router. The Flutter team maintains `go_router`:
final router = GoRouter(routes: [
GoRoute(path: '/', builder: (c, s) => const HomeScreen()),
GoRoute(path: '/item/:id', builder: (c, s) =>
ItemScreen(id: s.pathParameters['id']!)),
]);
MaterialApp.router(routerConfig: router);Navigate with context.go('/item/42') (replace the stack to match the URL) or context.push('/item/42') (add on top). redirect implements login guards, and StatefulShellRoute keeps per-tab stacks.
On Android, PopScope intercepts the system back gesture, e.g. to confirm discarding a draft. After awaiting a pushed route, check context.mounted before using context again.
Example
import 'package:flutter/material.dart';
class ColorPicker extends StatelessWidget {
const ColorPicker({super.key});
@override
Widget build(BuildContext context) => Scaffold(
appBar: AppBar(title: const Text('Pick a color')),
body: ListView(children: [
for (final (name, color) in [('Red', Colors.red), ('Blue', Colors.blue)])
ListTile(title: Text(name), onTap: () => Navigator.pop(context, color)),
]),
);
}
class Home extends StatelessWidget {
const Home({super.key});
@override
Widget build(BuildContext context) => FilledButton(
child: const Text('Choose color'),
onPressed: () async {
final color = await Navigator.push<Color>(
context, MaterialPageRoute(builder: (_) => const ColorPicker()));
if (!context.mounted || color == null) return; // user may have gone back
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Got $color')));
},
);
}Remember
pushreturns a Future completed bypop(context, result)- Pass data to screens through constructor parameters
- Use go_router for URLs, deep links, redirects and tab stacks
Watch out for
- Using
contextafter awaiting a route without checkingcontext.mounted.