skip to content

Navigation & Routing

Moving between screens with the imperative Navigator, the declarative Router API and go_router, plus shell routes, deep links and page transitions. Interviewers probe where push/pop stops scaling.

part ofFlutteroverview, primer and where to startread it →
on this pageshow

explore

questions

30

In Flutter, how do you open an address-picker screen with Navigator.push and receive the address the user chooses?

level: juniorimportance: must knowfreq 74%

answer

  1. push returns a Future
  2. pop carries the value back
  3. type the route: MaterialPageRoute<Address>
  4. back button completes with null
  5. check mounted after the await

basics

~10 s

Navigator.push returns a Future<T?> that completes when the pushed route is popped. The picker calls Navigator.pop(context, address); the caller awaits push and gets that address, or null if the user went back without choosing.

solid answer

~40 s

`Navigator.push<Address>(context, MaterialPageRoute<Address>(builder: ...))` puts the picker on top of the navigator's stack and returns a `Future<Address?>`. When the user taps an address, the picker calls `Navigator.pop(context, address)`, which removes the picker and completes that future with the address. The caller simply awaits it. The result is nullable because the user can also leave with the back button, the app-bar back arrow or a back gesture, which pops with no value, so the caller must handle `null`. Typing the route with `<Address>` makes the future typed, and after the await the caller checks `mounted` before calling `setState`, because the screen may have been disposed while the picker was open.

code

dart · 59 lines
dart
import 'package:flutter/material.dart';

class Address {
  const Address(this.label);
  final String label;
}

class CheckoutScreen extends StatefulWidget {
  const CheckoutScreen({super.key});

  @override
  State<CheckoutScreen> createState() => _CheckoutScreenState();
}

class _CheckoutScreenState extends State<CheckoutScreen> {
  Address? _address;

  Future<void> _chooseAddress() async {
    final Address? picked = await Navigator.push<Address>(
      context,
      MaterialPageRoute<Address>(builder: (context) => const AddressPickerScreen()),
    );
    if (!mounted || picked == null) return; // null: the user went back without choosing
    setState(() => _address = picked);
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Checkout')),
      body: ListTile(
        title: Text(_address?.label ?? 'Choose a delivery address'),
        onTap: _chooseAddress,
      ),
    );
  }
}

class AddressPickerScreen extends StatelessWidget {
  const AddressPickerScreen({super.key});

  static const List<Address> _saved = [Address('Home'), Address('Office')];

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Deliver to')),
      body: ListView(
        children: [
          for (final address in _saved)
            ListTile(
              title: Text(address.label),
              onTap: () => Navigator.pop(context, address),
            ),
        ],
      ),
    );
  }
}

go deeper

for a junior

Recall that push returns a Future, pop(context, value) completes it, and back navigation gives null.

for a middle

Explain why the Future is nullable, how route type arguments make the result typed, and why mounted is checked after the await.

for a senior

Show how you wrap result-returning screens in typed helpers so the contract is enforced at every call site.

for a principal

Weigh result-returning routes against shared state for flows like checkout, where several screens contribute to one order.

## The Navigator as a stack In Flutter, a **`Navigator`** is a widget that manages a stack of **routes**. A route is an entry on that stack, usually a full screen; **`MaterialPageRoute`** is the standard route for a page with the platform's page transition. The imperative API changes the stack directly: - **`Navigator.push(context, route)`** adds a route on top; - **`Navigator.pop(context, [result])`** removes the top route, optionally handing back a value. Every route has a **result type** `T`, and `push` returns a **`Future<T?>`**. That future completes when the pushed route leaves the stack. This is what makes "open a screen, get a value back" a single `await`. ## The delivery-address flow 1. The checkout screen calls `Navigator.push<Address>(context, MaterialPageRoute<Address>(builder: (context) => const AddressPickerScreen()))`. 2. The picker is shown on top of checkout. Checkout's state is kept; its `await` is pending. 3. The user taps "Office". The picker calls `Navigator.pop(context, address)`. 4. The picker animates out, and the future checkout is awaiting completes with that `Address`. 5. Checkout checks `mounted`, then stores the address with `setState`. ```dart Future<void> _chooseAddress() async { final Address? picked = await Navigator.push<Address>( context, MaterialPageRoute<Address>(builder: (context) => const AddressPickerScreen()), ); if (!mounted || picked == null) return; setState(() => _address = picked); } ``` ## Why the result is nullable The future's type is `Future<T?>`, not `Future<T>`, because a route can leave the stack without a value: | How the picker leaves | Checkout receives | |---|---| | `Navigator.pop(context, address)` | the address | | Android back button or back gesture | `null` | | App-bar back arrow (it calls `maybePop` with no value) | `null` | | Removed by another navigation call, e.g. `pushAndRemoveUntil` | `null` (the route's default result) | So the caller must treat `null` as "the user cancelled", never as an error. ## Typing the route Writing the type on both `push<Address>` and `MaterialPageRoute<Address>` gives three benefits: - the awaited value is an `Address?`, so no cast is needed; - the picker cannot silently return a string: in debug builds, popping a `MaterialPageRoute<Address>` with a value of another type throws a `FlutterError` ("A request was made to pop a route with a result of type ..."); - readers can see the screen's contract from the call site. Without type arguments the route is `MaterialPageRoute<dynamic>`, the future is `Future<dynamic>`, and a typo in the value popped only surfaces later, somewhere else. ## Using context after the await The `await` spans the whole time the picker is open. During that time the checkout screen could be removed (for example the user signed out). Before touching `setState` or the `BuildContext` again, check `mounted` in a `State` (or `context.mounted` elsewhere). This rule applies to every awaited navigation, not only to this flow. ## Common mistakes - Calling `setState` in the picker expecting checkout to update: each screen has its own `State`; the value has to travel through `pop`. - Pushing a new checkout screen with the address instead of popping: that stacks a second checkout on top of the first. - Ignoring `null`, which crashes with a null-check error when the user presses back. - Passing results through a global variable, which breaks as soon as two pickers are opened in sequence. ## Variants worth knowing - `Navigator.of(context).push(route)` is the same call as the static `Navigator.push(context, route)`; the static form looks up the nearest navigator for you. - `Navigator.maybePop(context, value)` delivers a value too, but only if the route agrees to pop (a `PopScope` with `canPop: false` can refuse). - `Navigator.popUntilWithResult(context, predicate, value)`, added in Flutter 3.41, pops several routes and hands `value` to the last one popped, useful when a nested flow should return one answer to the screen that started it. - Dialogs and bottom sheets are routes as well, so the same "await the push, pop with a value" contract applies to them.

  • What does the caller receive if the picker calls Navigator.pop(context) without an argument?
    `null`. `pop` without a value completes the pushed route's future with the route's default result, which is `null` for `MaterialPageRoute`. That is the same thing the caller sees when the user presses back, so both mean "no address chosen".
  • How would you give the picker a reusable, typed entry point?
    Add a static helper on the picker, for example `static Future<Address?> pick(BuildContext context)`, that performs the typed `Navigator.push<Address>` with `MaterialPageRoute<Address>`. Callers then write `await AddressPickerScreen.pick(context)` and cannot get the route type or result type wrong.

A coat-check ticket: push hands you the ticket (the Future) as the attendant walks away; when they come back (pop) they hand over the coat, or nothing if they return empty-handed.

saying these in an interview costs you the question

  • Expects push to return the chosen address synchronously
  • Treats a null result from push as an error
  • Updates the caller by calling setState inside the picker
  • Pushes a new checkout screen to carry the address back
  • Omits the type argument and casts the dynamic result later
open as a page

Why does Flutter offer the declarative Router API (MaterialApp.router) alongside Navigator.push, and when does an app actually need it?

level: juniorimportance: must knowfreq 55%

basics

~20 s

The Router API derives the navigator's stack from app state: a URL is parsed into state, state builds Navigator.pages, and state is reported back as a URL. Apps need it for deep links that rebuild the stack and web history.

open as a page

In Flutter, how do you give one pushed route a custom slide-up transition with PageRouteBuilder, and what do its two animations drive?

level: juniorimportance: must knowfreq 55%

basics

~20 s

Push a PageRouteBuilder whose pageBuilder returns the screen and whose transitionsBuilder wraps the child in a transition driven by animation, which runs 0 to 1 on push and back on pop; secondaryAnimation runs when another route covers it.

open as a page

With go_router, how do you redirect signed-out users of a banking app to a login route and send them back after sign-in?

level: middleimportance: must knowfreq 62%

basics

~20 s

Give GoRouter a top-level redirect that returns '/login' for a signed-out user and null otherwise, and pass the session ChangeNotifier as refreshListenable so the redirect runs again the moment the user signs in or out.

open as a page

With go_router, what is the difference between context.go and context.push, and when would you use each?

level: middleimportance: must knowfreq 72%

basics

~20 s

context.go replaces the whole page stack with the pages the destination's route hierarchy defines and updates the URL; context.push adds the matched page on top of the current stack and returns a Future that completes with the value passed to pop.

open as a page

In Flutter, how does PopScope stop the back gesture from closing a half-filled address form, and how do you confirm before discarding?

level: middleimportance: must knowfreq 52%

basics

~20 s

Wrap the form in PopScope with canPop false while it has unsaved input. A back gesture then leaves the route in place and calls onPopInvokedWithResult with didPop false; show a dialog there and call Navigator.pop if the user confirms.

open as a page

With go_router, how do you show a full-screen page above the bottom navigation bar from a route nested inside a shell?

level: middleimportance: must knowfreq 52%

basics

~20 s

Give GoRouter a GlobalKey<NavigatorState> as navigatorKey and set that same key as the parentNavigatorKey of the nested GoRoute; the page is then placed on the root Navigator above the shell, hiding the bar, while its URL stays nested.

open as a page

In a Flutter app, which route choices disable the iOS edge swipe-back gesture, and how do you keep it alongside a custom transition?

level: middleimportance: must knowfreq 50%

basics

~20 s

The swipe is a detector inside Flutter's Cupertino page transition, so PageRouteBuilder or CustomTransitionPage routes lack it. It is also off for fullscreenDialog routes, the first route, a route with PopScope canPop false, and while a transition runs.

open as a page

With go_router, how do you pass data to a route through path parameters, query parameters and extra, and read each one?

level: juniorimportance: should knowfreq 55%

basics

~10 s

Declare a :accountId segment in the GoRoute path and read state.pathParameters['accountId']; append ?month=2026-09 and read state.uri.queryParameters['month']; pass an object with extra: and read state.extra, which never appears in the URL.

open as a page

With go_router, how does ShellRoute keep a bottom navigation bar on screen while the pages under it change?

level: juniorimportance: should knowfreq 45%

basics

~20 s

ShellRoute has no path; it wraps its sub-routes in a widget you build, and its builder receives a child that is a nested Navigator showing the matched sub-route, so the scaffold with the navigation bar stays while pages change inside it.

open as a page

In Flutter, what changes when you push a MaterialPageRoute or CupertinoPageRoute with fullscreenDialog: true?

level: juniorimportance: should knowfreq 28%

basics

~20 s

The route becomes a full-screen modal task: Material app bars show a close button, Cupertino nav bars a Cancel button, iOS slides it up from the bottom, the screen below stays still, and pop gestures are disabled.

open as a page

Why does the Flutter documentation discourage named routes for most apps, and what does onGenerateRoute add over the MaterialApp routes table?

level: middleimportance: should knowfreq 42%

basics

~20 s

Named routes always push a new route for a deep link and lack browser forward support, so the docs suggest go_router or MaterialPageRoute. onGenerateRoute builds routes in code, so it can type results and parse arguments.

open as a page

In Flutter's Navigator, how do pushReplacement and pushAndRemoveUntil differ, and when would you use each in a delivery-ordering flow?

level: middleimportance: should knowfreq 46%

basics

~10 s

pushReplacement swaps the top route for a new one; pushAndRemoveUntil pushes a new route and removes routes beneath it until a predicate returns true. Use replacement after login, and remove-until after placing an order.

open as a page

In Flutter's Navigator.pages API, how do Page objects become routes, why do page keys matter, and what must onDidRemovePage do?

level: middleimportance: should knowfreq 36%

basics

~20 s

Each Page creates a route via createRoute, and the Navigator diffs the new pages list against the old one by runtimeType and key. Give each distinct screen a unique key, and remove the popped page from your state in onDidRemovePage.

open as a page

In Flutter's Router API, what do RouteInformationParser and RouterDelegate each do when a docs-app URL is opened and when the user navigates in-app?

level: middleimportance: should knowfreq 32%

basics

~20 s

On a URL, the parser's parseRouteInformation builds a typed configuration and the delegate's setNewRoutePath applies it. In-app, the delegate changes state and notifies; the Router reads currentConfiguration and the parser's restoreRouteInformation turns it into the new URL.

open as a page

In Flutter 3.47, how does ThemeData.pageTransitionsTheme choose a MaterialPageRoute's transition on each platform, and what are the defaults?

level: middleimportance: should knowfreq 40%

basics

~10 s

MaterialPageRoute asks the theme's PageTransitionsTheme for the builder mapped to Theme.of(context).platform. The 3.47 defaults are PredictiveBackPageTransitionsBuilder on Android, CupertinoPageTransitionsBuilder on iOS and macOS, and ZoomPageTransitionsBuilder on Windows and Linux.

open as a page

A go_router app shows its error screen with 'redirect loop detected' or 'too many redirects' after a sign-in change; how do you diagnose and fix it?

level: seniorimportance: should knowfreq 30%

basics

~20 s

go_router records each location a navigation's redirects produce: a repeated location throws 'redirect loop detected', and more hops than redirectLimit (default 5) throws 'too many redirects'. Turn on debugLogDiagnostics, find the guards that disagree, and give the chain a fixed point.

open as a page

A Flutter checkout awaits Navigator.push for an address picker, yet sometimes receives null although the user saved a new address; what causes this and how do you fix it?

level: seniorimportance: should knowfreq 30%

basics

~20 s

The awaited Future belongs to the picker route, so pushReplacement or a remove-until call that ends the picker early completes it with null. Push the form on top of the picker, await it, then pop the picker with the result.

open as a page

A Flutter web docs app built on a custom RouterDelegate never updates the address bar, and the browser back button leaves the site; what is wrong and how do you fix it?

level: seniorimportance: should knowfreq 24%

basics

~20 s

The Router never reports route information, so the browser gets no history entries. Usually currentConfiguration or restoreRouteInformation still returns null, or state changes skip notifyListeners, or screens are opened with Navigator.push. Override both methods, notify, and put pages in state.

open as a page

In a go_router app with StatefulShellRoute tabs, which Navigator does the Android back button pop, and why does it not return to the previous tab?

level: seniorimportance: should knowfreq 36%

basics

~20 s

go_router's delegate tries maybePop on the innermost active Navigator first, the current branch's, then works outward to the root; a tab switch is not a page on any stack, so at a branch's root nothing pops and the platform's default back, leaving the app, applies.

open as a page

With go_router, how do you build bottom tabs in a social app so each tab keeps its own navigation history?

level: seniorimportance: should knowfreq 48%

basics

~10 s

Use StatefulShellRoute.indexedStack with one StatefulShellBranch per tab: each branch gets its own Navigator kept alive in an IndexedStack, and the tab bar calls navigationShell.goBranch(index), which restores that branch's last location instead of rebuilding it.

open as a page

In a Flutter 3.47 app, what must be in place for Android predictive back to preview the previous route, and why can a custom transition lose that preview?

level: seniorimportance: should knowfreq 35%

basics

~20 s

Add android:enableOnBackInvokedCallback="true" to the manifest, run on Android 14 or later, and keep routes on PredictiveBackPageTransitionsBuilder, the default for MaterialPageRoute since 3.38. Custom builders never claim the gesture, so the route just pops on release.

open as a page

With go_router_builder, what do typed routes built from TypedGoRoute and GoRouteData give you over string paths?

level: middleimportance: nice to knowfreq 24%

basics

~20 s

go_router_builder reads @TypedGoRoute annotations on GoRouteData subclasses and generates a mixin with location, go, push and replace plus a $appRoutes list, so path and query parameters become typed constructor fields and a missing or mistyped one is a compile-time error.

open as a page

In Flutter's Router API, how does a BackButtonDispatcher route the Android back button, and when do you need a ChildBackButtonDispatcher?

level: middleimportance: nice to knowfreq 16%

basics

~10 s

RootBackButtonDispatcher, the default for MaterialApp.router, receives Android back presses and calls the delegate's popRoute, usually maybePop on its navigator. A nested Router needs a ChildBackButtonDispatcher that calls takePriority to get presses first.

open as a page

With go_router 18, how does the top-level onEnter callback differ from redirect, and when would you choose it?

level: seniorimportance: nice to knowfreq 18%

basics

~20 s

onEnter runs once per navigation before any redirect, sees both the current and the next GoRouterState, and returns Allow or Block, so it can cancel a navigation and keep the user where they are; redirect only lets a navigation through or sends it elsewhere.

open as a page