Navigation & Routing
Navigator, named routes, go_router, passing data between screens
- 01
Explain Navigator 1.0 (imperative) vs go_router (declarative).
EasyNavigator is an imperative stack you push and pop, while
go_routerdeclares the whole route table and drives that stack for you.// ── Navigator 1.0 ───────────────────────────────────── // Push Navigator.of(context).push( MaterialPageRoute(builder: (_) => const DetailScreen(id: 42)), ); // Pop with value … - 02
How do you implement bottom navigation with persistent state in Flutter?
MediumBottom navigation should preserve state in each tab — users expect the Home tab to still be scrolled where they left it.
// ── IndexedStack approach (simplest) ────────────── class MainNav extends StatefulWidget { const MainNav({super.key}); @override State<MainNav> createState() => _MainNavState(); } … - 03
How do you pass data between screens and get results back?
MediumConstructor arguments are the type-safe default for sending data forward, and popping with a value sends a result back to whoever awaited the push.
// ── Method 1: Constructor (recommended) ──── class DetailsPage extends StatelessWidget { final String userId; final int age; const DetailsPage({required this.userId, required this.age}); … - 04
What is PopScope (formerly WillPopScope) and how do you intercept the back button?
MediumPopScope controls whether the current route may be popped and lets you react when the user tries to leave.
class EditProfilePage extends StatefulWidget { const EditProfilePage({super.key}); @override State<EditProfilePage> createState() => _EditProfilePageState(); } class _EditProfilePageState extends State<EditProfilePage> { … - 05
How do Hero animations work, and what are the gotchas?
MediumA Hero is a widget shared between two routes that animates in flight when you push or pop.
// Source — list tile class ProductTile extends StatelessWidget { const ProductTile({super.key, required this.product}); final Product product; @override Widget build(BuildContext context) { … - 06
How do you guard routes for authentication with go_router?
Hardgo_router exposes
redirectcallbacks that run before a route is built.import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; final authProvider = StateNotifierProvider<AuthNotifier, AuthState>( (ref) => AuthNotifier(), ); … - 07
How do showDialog, showModalBottomSheet, and showMenu fit into the navigation stack?
MediumThese three helpers all push a modal route onto the current navigator, so what looks like an overlay is really a route sitting on the stack.
// ── showDialog ── final confirmed = await showDialog<bool>( context: context, barrierDismissible: false, useRootNavigator: true, builder: (_) => AlertDialog( … - 08
How do you set up deep linking and universal links for a Flutter app?
HardDeep links open a specific screen in your app from a URL, and there are three flavours with very different reach.
// pubspec: app_links: ^6.0.0 import 'package:app_links/app_links.dart'; final _appLinks = AppLinks(); Future<void> bootstrapLinks(GoRouter router) async { … - 09
How do you build a custom page transition animation?
MediumDefault page transitions are platform-specific: MaterialPageRoute zooms and fades on Android and slides in from the right on iOS.
// ── PageRouteBuilder — fade + scale transition ── Route<T> fadeScaleRoute<T>(WidgetBuilder builder) { return PageRouteBuilder<T>( pageBuilder: (context, anim, secondary) => builder(context), transitionDuration: const Duration(milliseconds: 280), reverseTransitionDuration: const Duration(milliseconds: 220), … - 10
In go_router, what is the difference between
context.go()andcontext.push()?Mediumgo replaces the current stack with the one your route tree implies for the target location, while push puts the new page on top of whatever is already there.
// Switch destination — stack is rebuilt from the route tree context.go('/orders'); context.goNamed('order', pathParameters: {'id': order.id}); // Stack a detail screen and wait for its result final saved = await context.push<bool>('/orders/${order.id}/edit'); if (saved == true) _reload(); … - 11
After a successful login, how do you stop the back button from returning to the login screen?
EasyReplace the login route instead of pushing on top of it, so that no login entry is left in the stack.
// Only the top route is swapped Navigator.pushReplacement( context, MaterialPageRoute(builder: (_) => const HomePage()), ); … - 12
A push notification is tapped and you must open a specific screen. How do you navigate with no BuildContext to hand?
MediumKeep a GlobalKey for the Navigator, or a reference to the GoRouter itself, and navigate through that from outside the widget tree.
final navigatorKey = GlobalKey<NavigatorState>(); final scaffoldMessengerKey = GlobalKey<ScaffoldMessengerState>(); MaterialApp( navigatorKey: navigatorKey, scaffoldMessengerKey: scaffoldMessengerKey, … - 13
Your button calls
Navigator.of(context).pushand throws "Navigator operation requested with a context that does not include a Navigator" — why?EasyThe context you passed sits above the Navigator, not below it —
Navigator.ofonly searches upwards, and MaterialApp builds its Navigator inside itself.// WRONG — this context is the parent of MaterialApp class App extends StatelessWidget { const App({super.key}); @override Widget build(BuildContext context) { return MaterialApp( … - 14
You await Navigator.push, then use the same context for a SnackBar, and it throws about looking up a deactivated widget's ancestor — what is the rule?
EasyAn
awaithands control back to the framework, and by the time the code resumes the widget that owned that context may already be gone from the tree, so the context can no longer be used to look anything up.// WRONG — context is used after two async gaps Future<void> _openEditor(BuildContext context, Order order) async { final saved = await Navigator.push<bool>( context, MaterialPageRoute(builder: (_) => EditorPage(order: order)), ); … - 15
You push a detail screen over a video player and the video keeps playing underneath — is the screen below still in the widget tree?
MediumYes — pushing a route does not dispose the route below it: that screen stays mounted, keeps its State and keeps rebuilding, it just stops being painted.
// One observer for the whole app final routeObserver = RouteObserver<ModalRoute<void>>(); MaterialApp( navigatorObservers: [routeObserver], home: const PlayerPage(), … - 16
Your Flutter web build shows URLs like example.com/#/orders/42, and after switching to clean paths a hard refresh 404s — what is going on?
MediumFlutter web defaults to the hash URL strategy, which keeps the whole route after a
#so the server never sees it; the path strategy gives real URLs but the server must then serveindex.htmlfor paths it does not have.import 'package:flutter_web_plugins/url_strategy.dart'; void main() { // Must run before runApp; no-op on mobile, so no conditional import usePathUrlStrategy(); // /orders/42 instead of /#/orders/42 runApp(const App()); … - 17
On a slow phone a user double-taps a list row and two identical detail screens land on the stack — how do you stop that?
MediumMake the second call a no-op by guarding the navigation itself; the Navigator will never deduplicate for you, because two routes built from the same widget are genuinely two different routes.
// WRONG — two taps, two identical routes ListTile( title: Text(order.title), onTap: () async { final full = await api.load(order.id); // 800 ms on a bad network if (!context.mounted) return; … - 18
You need a named route like /product/42, but MaterialApp.routes only matches exact strings — how do you handle a parameterised route?
MediumonGenerateRouteis the callback MaterialApp calls for any name theroutestable does not match: you receive theRouteSettings, parse the name yourself and return aRoute.MaterialApp( initialRoute: '/', routes: { '/': (_) => const HomePage(), '/settings': (_) => const SettingsPage(), // static names only }, … - 19
With a page-based Navigator you insert a page in the middle of the list and the wrong screen keeps its scroll position — what is missing?
HardKeys — the Navigator pairs the new
pageslist against the old one usingPage.canUpdate, which comparesruntimeTypeandkey, so keyless pages of the same type are interchangeable and state follows position instead of identity.class AppRouterDelegate extends RouterDelegate<Object> with ChangeNotifier, PopNavigatorRouterDelegateMixin<Object> { @override final navigatorKey = GlobalKey<NavigatorState>(); final List<Order> _openOrders = []; … - 20
Android kills your backgrounded app, the user returns and lands on the home screen with a three-deep stack lost — how do you bring it back?
HardTurn on Flutter's state restoration: give the app a
restorationScopeIdand push with therestorable*Navigator methods, so the framework serialises the route stack and any state you registered and rebuilds it after process death.void main() => runApp(const MaterialApp( restorationScopeId: 'app', // without this, nothing restores home: OrderListPage(), )); // Top-level or static: serialised as a pointer to this function …