skip to content

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