skip to content

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%

answer

  1. swap the top vs clear down to a point
  2. login screen should not stay behind home
  3. predicate: ModalRoute.withName or route.isFirst
  4. (route) => false empties the stack
  5. replaced route's future gets result:

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.

solid answer

~40 s

`Navigator.pushReplacement(context, newRoute)` pushes a new route and disposes the current top route once the new one has animated in, so Back skips the replaced screen; a login screen replaced by home is the classic case. It takes an optional `result:` that completes the replaced route's own push future. `Navigator.pushAndRemoveUntil(context, newRoute, predicate)` pushes a new route and removes every route beneath it until `predicate` returns true: after an order is placed, `(route) => route.isFirst` leaves only home under the tracking screen, and `(route) => false` removes everything. `ModalRoute.withName('/')` works as a predicate only for routes that carry a name in their `RouteSettings`. Futures of removed routes complete, with `null` by default.

code

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

void onLoginSucceeded(BuildContext context) {
  Navigator.pushReplacement<void, void>(
    context,
    MaterialPageRoute<void>(builder: (context) => const HomeScreen()),
  );
}

void onOrderPlaced(BuildContext context) {
  Navigator.pushAndRemoveUntil<void>(
    context,
    MaterialPageRoute<void>(builder: (context) => const OrderTrackingScreen()),
    (Route<dynamic> route) => route.isFirst,
  );
}

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

  @override
  Widget build(BuildContext context) => const Scaffold(body: Center(child: Text('Home')));
}

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

  @override
  Widget build(BuildContext context) => const Scaffold(body: Center(child: Text('Tracking')));
}

go deeper

for a junior

Recall that pushReplacement swaps the top screen and pushAndRemoveUntil clears screens below a new one until a predicate matches.

for a middle

Explain the predicates, why ModalRoute.withName needs named routes, and what the removed or replaced routes' futures complete with.

for a senior

Show how you design stack resets for login, order completion and sign-out so Back never lands on an obsolete or unauthenticated screen.

for a principal

Judge when imperative stack surgery becomes brittle enough that a declarative router deriving the stack from app state is the better model.

## Two ways to rewrite the stack `Navigator.push` only ever adds. Real flows also need to **forget** screens, so the user's Back button does not lead somewhere meaningless. Flutter's `Navigator` has two push variants for that. | Method | What it does to the stack | Typical use | |---|---|---| | `pushReplacement(context, route, {result})` | pushes `route`, removes the route that was on top | login → home; splash → onboarding | | `pushAndRemoveUntil(context, route, predicate)` | pushes `route`, removes routes below it until `predicate` is true | order placed → tracking with only home below; sign-out → login with nothing below | Both return the new route's `Future<T?>`, like `push`. ## pushReplacement `pushReplacement` pushes the new route and **disposes the old top route after the new one has finished animating in**. The old route does not play an exit animation (`popAndPushNamed` is the variant that animates the old route out). Key details: - The type parameters are `<T, TO>`: `T` is the new route's result type, `TO` the old route's. - The optional **`result:`** completes the old route's push future. Whoever awaited `push` for the old route receives that value (or `null` if none was given) at the moment of replacement, not when the new route is later popped. - Back from the new route goes to whatever was below the replaced route. ```dart // Login succeeded: home replaces login, so Back cannot return to it. Navigator.pushReplacement<void, void>( context, MaterialPageRoute<void>(builder: (context) => const HomeScreen()), ); ``` ## pushAndRemoveUntil `pushAndRemoveUntil` walks down the stack from the top, removing routes until the **predicate** returns `true` for one; that route and everything beneath it stay. The removed routes are disposed once the new route has animated in, and **the futures returned from pushing them complete**, with each route's default result, which is `null` for page routes. Common predicates: 1. `(Route<dynamic> route) => route.isFirst` keeps only the bottom route, typically home. 2. `ModalRoute.withName('/')` keeps routes down to the one named `/`. It matches on `route.settings.name`, so it only finds routes that were given a name: the `home` route of `MaterialApp` is named `/`, but a `MaterialPageRoute` pushed without `settings` has no name. 3. `(Route<dynamic> route) => false` removes every existing route, leaving the new route alone on the stack. ```dart // Order placed: tracking screen on top of home; cart, checkout and picker are gone. Navigator.pushAndRemoveUntil<void>( context, MaterialPageRoute<void>(builder: (context) => const OrderTrackingScreen()), (Route<dynamic> route) => route.isFirst, ); ``` ## Related calls - **`popUntil(context, predicate)`** pops without pushing, e.g. back to home after cancelling checkout. - **`maybePop(context)`** pops only if the route allows it, which respects a `PopScope` with `canPop: false`. - Named variants (`pushReplacementNamed`, `pushNamedAndRemoveUntil`) do the same through route names and need route generation configured on the app. ## What observers see A `NavigatorObserver` (used for analytics or logging) is told about these calls differently from a pop: `pushReplacement` reports `didReplace`, and routes removed by `pushAndRemoveUntil` are reported through `didRemove`, alongside `didPush` for the new route. An observer that only tracks `didPush` and `didPop` will think the removed screens are still on the stack, a common source of wrong "current screen" data. ## Choosing in a delivery app - **Splash or login to home**: `pushReplacement`. There is exactly one screen to forget. - **Checkout to order tracking**: `pushAndRemoveUntil` with `isFirst`. Cart, checkout and the address picker are all obsolete; home stays so Back behaves. - **Sign-out**: `pushAndRemoveUntil` with `(route) => false`, so no signed-in screen survives beneath login. - **Address picker to "add address" form**: usually plain `push`. Replacing the picker completes checkout's pending future immediately, so the new address would never reach checkout. ## Pitfalls - Using `ModalRoute.withName` for a route that was pushed without `RouteSettings(name: ...)`: the predicate never matches, so every route is removed. - Replacing a route someone is awaiting and expecting the new route's result to arrive there. - Removing every route beneath the new one on Android and being surprised that Back from it leaves the app: with nothing below to pop, the back press is handed to the system.

  • Why might ModalRoute.withName('/checkout') remove every route instead of stopping at checkout?
    The predicate compares `route.settings.name`. If checkout was pushed as `MaterialPageRoute(builder: ...)` without `settings: RouteSettings(name: '/checkout')`, it has no name, the predicate never returns true, and `pushAndRemoveUntil` removes all routes below the new one.
  • What happens to a screen that is awaiting the push Future of a route removed by pushAndRemoveUntil?
    The Future completes once the removed route is disposed, with that route's default result, `null` for page routes. If the awaiting screen was itself removed, it must check `mounted` before using its context.

saying these in an interview costs you the question

  • Thinks pushReplacement pops the old route with its exit animation
  • Believes ModalRoute.withName matches routes pushed without a name
  • Expects the replaced route's awaiter to get the new route's result
  • Uses push after login, leaving login behind home
  • Thinks removed routes' push futures never complete