In Flutter's Navigator, how do pushReplacement and pushAndRemoveUntil differ, and when would you use each in a delivery-ordering flow?
answer
- swap the top vs clear down to a point
- login screen should not stay behind home
- predicate: ModalRoute.withName or route.isFirst
- (route) => false empties the stack
- replaced route's future gets result:
basics
~10 spushReplacement 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 linesimport '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
Recall that pushReplacement swaps the top screen and pushAndRemoveUntil clears screens below a new one until a predicate matches.
Explain the predicates, why ModalRoute.withName needs named routes, and what the removed or replaced routes' futures complete with.
Show how you design stack resets for login, order completion and sign-out so Back never lands on an obsolete or unauthenticated screen.
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