Navigation & Routing
Irbisa · cheatsheetSeptember 13, 2026

Navigation & Routing

Navigator, named routes, go_router, passing data between screens

Junior Developer20 itemscompressed for a skim
  1. 01

    Explain Navigator 1.0 (imperative) vs go_router (declarative).

    Easy

    Navigator is an imperative stack you push and pop, while go_router declares 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
    …
  2. 02

    How do you implement bottom navigation with persistent state in Flutter?

    Medium

    Bottom 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();
    }
    …
  3. 03

    How do you pass data between screens and get results back?

    Medium

    Constructor 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});
    …
  4. 04

    What is PopScope (formerly WillPopScope) and how do you intercept the back button?

    Medium

    PopScope 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> {
    …
  5. 05

    How do Hero animations work, and what are the gotchas?

    Medium

    A 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) {
    …
  6. 06

    How do you guard routes for authentication with go_router?

    Hard

    go_router exposes redirect callbacks 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(),
    );
    …
  7. 07

    How do showDialog, showModalBottomSheet, and showMenu fit into the navigation stack?

    Medium

    These 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(
    …
  8. 08

    How do you set up deep linking and universal links for a Flutter app?

    Hard

    Deep 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 {
    …
  9. 09

    How do you build a custom page transition animation?

    Medium

    Default 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. 10

    In go_router, what is the difference between context.go() and context.push()?

    Medium

    go 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. 11

    After a successful login, how do you stop the back button from returning to the login screen?

    Easy

    Replace 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. 12

    A push notification is tapped and you must open a specific screen. How do you navigate with no BuildContext to hand?

    Medium

    Keep 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. 13

    Your button calls Navigator.of(context).push and throws "Navigator operation requested with a context that does not include a Navigator" — why?

    Easy

    The context you passed sits above the Navigator, not below it — Navigator.of only 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. 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?

    Easy

    An await hands 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. 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?

    Medium

    Yes — 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. 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?

    Medium

    Flutter 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 serve index.html for 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. 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?

    Medium

    Make 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. 18

    You need a named route like /product/42, but MaterialApp.routes only matches exact strings — how do you handle a parameterised route?

    Medium

    onGenerateRoute is the callback MaterialApp calls for any name the routes table does not match: you receive the RouteSettings, parse the name yourself and return a Route.

    MaterialApp(
      initialRoute: '/',
      routes: {
        '/': (_) => const HomePage(),
        '/settings': (_) => const SettingsPage(),   // static names only
      },
    …
  19. 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?

    Hard

    Keys — the Navigator pairs the new pages list against the old one using Page.canUpdate, which compares runtimeType and key, 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. 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?

    Hard

    Turn on Flutter's state restoration: give the app a restorationScopeId and push with the restorable* 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
    …