skip to content

Page Transitions & Back

PageRouteBuilder's transitionsBuilder animates a route in, PageTransitionsTheme picks one per platform, and Android predictive back previews the pop. Interviewers ask what breaks iOS swipe-back.

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

explore

questions

5

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%

answer

  1. callbacks instead of a PageRoute subclass
  2. pageBuilder builds, transitionsBuilder wraps child
  3. animation: this route's own push and pop
  4. secondaryAnimation: a route covering this one
  5. 300 ms each way by default

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.

solid answer

~40 s

`PageRouteBuilder` is a ready-made `PageRoute` configured with callbacks. `pageBuilder(context, animation, secondaryAnimation)` builds the screen, and the route caches that page instead of rebuilding it every frame. `transitionsBuilder(context, animation, secondaryAnimation, child)` rebuilds on every animation tick and wraps the cached `child`, so a slide-up is a `SlideTransition` driven by `animation` from `Offset(0, 1)` to `Offset.zero`. `animation` runs 0 to 1 while this route is pushed and 1 to 0 while it pops; `secondaryAnimation` runs while a later route is pushed over it or popped off it. The defaults: 300 ms `transitionDuration` and `reverseTransitionDuration`, `opaque: true`, and a default `transitionsBuilder` that returns the child untouched, so nothing visibly animates. The cost: a plain `PageRouteBuilder` has no iOS edge swipe-back and no Android predictive-back preview.

code

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

Route<void> matchProfileRoute(Widget profile) {
  return PageRouteBuilder<void>(
    transitionDuration: const Duration(milliseconds: 350),
    reverseTransitionDuration: const Duration(milliseconds: 250),
    pageBuilder: (context, animation, secondaryAnimation) => profile,
    transitionsBuilder: (context, animation, secondaryAnimation, child) {
      final Animation<Offset> offset = Tween<Offset>(
        begin: const Offset(0, 1),
        end: Offset.zero,
      ).chain(CurveTween(curve: Curves.easeOutCubic)).animate(animation);
      return SlideTransition(position: offset, child: child);
    },
  );
}

// Navigator.of(context).push(matchProfileRoute(MatchProfileScreen(match: match)));

go deeper

for a junior

Recall the two callbacks, which one wraps the child, and that animation runs forward on push and backward on pop.

for a middle

Explain secondaryAnimation, the cached page versus the per-frame transitionsBuilder, and the 300 ms defaults and opaque flag.

for a senior

Show you know what the custom route loses: iOS swipe-back, the predictive-back preview and coordinated exit of the screen below.

for a principal

Weigh a one-off PageRouteBuilder against an app-wide PageTransitionsBuilder that keeps platform gestures where users expect them.

## What PageRouteBuilder is A **route** is one entry on a `Navigator` stack; a **page route** (`PageRoute<T>`) is a route that covers the whole screen. `MaterialPageRoute` and `CupertinoPageRoute` pick their animation for you. **`PageRouteBuilder<T>`** is the page route you reach for when one screen needs its own animation and you do not want to write a `PageRoute` subclass: you hand it two callbacks and a few settings, and it is pushed like any other route with `Navigator.of(context).push(...)`. The type argument `T` is the type of the result the route pops with. ## The two callbacks - **`pageBuilder`** — a `RoutePageBuilder`: `(BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation)`. It returns the screen itself. The route builds this page once and caches it, so the page is not rebuilt on every animation frame. - **`transitionsBuilder`** — a `RouteTransitionsBuilder`: the same three arguments plus `Widget child`, where `child` is the cached page. The route rebuilds this callback whenever either animation ticks, so it is where the moving parts go: a `SlideTransition`, `FadeTransition`, `ScaleTransition` or any combination of them. - **The default `transitionsBuilder` returns `child` unchanged.** The route's animation still runs for its duration, but nothing reads it, so the new screen simply appears. Supplying only `pageBuilder` is the usual way to get an instant, animation-free push. ## The two animations | Argument | Runs 0.0 to 1.0 when | Runs 1.0 to 0.0 when | |---|---|---| | `animation` | this route is pushed | this route is popped | | `secondaryAnimation` | another route is pushed on top of this one | that covering route is popped | `animation` animates the route **in and out**. `secondaryAnimation` lets the route react while it is **covered** — for example sliding slightly left while the next screen arrives. The covered route only receives a running `secondaryAnimation` when the two routes agree to coordinate: a `MaterialPageRoute` underneath animates only when the route above is also a Material-transition route or offers a delegated transition. A `PageRouteBuilder` offers neither by default, so the screen below it stays still. ## A slide-up in a dating app In a dating app, tapping a match card opens the full profile, which should rise from the bottom of the screen: ```dart Navigator.of(context).push( PageRouteBuilder<void>( pageBuilder: (context, animation, secondaryAnimation) => MatchProfileScreen(match: match), transitionsBuilder: (context, animation, secondaryAnimation, child) { final Animation<Offset> offset = Tween<Offset>( begin: const Offset(0, 1), end: Offset.zero, ).chain(CurveTween(curve: Curves.easeOutCubic)).animate(animation); return SlideTransition(position: offset, child: child); }, ), ); ``` What happens, in order: 1. `push` installs the route and starts its internal controller forward. 2. Each frame, `animation` advances and `transitionsBuilder` returns a `SlideTransition` at the new offset; the cached profile page rides inside it. 3. When the user taps back, `pop` runs the same controller in reverse over `reverseTransitionDuration`, so the profile slides back down. ## Defaults and knobs - `transitionDuration` and `reverseTransitionDuration` — both **300 ms** by default. - `opaque` — **true**: routes underneath stop painting once the transition finishes. Set it to false for a translucent overlay whose background still shows. - `barrierDismissible` — **false**; `barrierColor` and `barrierLabel` — null. - `maintainState` — **true**: the route keeps its state while covered. - `fullscreenDialog` — **false**; `allowSnapshotting` — **true**. ## What you give up The platform transitions carry behaviour, not just visuals. **iOS edge swipe-back** is a gesture detector built into the Cupertino page transition, and **Android predictive back** is a gesture listener built into `PredictiveBackPageTransitionsBuilder`. A `PageRouteBuilder` replaces the whole transition with your callback, so neither exists on that route: iOS users cannot swipe the profile away, and on Android a back gesture pops it only after the user releases, with your reverse animation instead of a live preview. Use `PageRouteBuilder` for a genuinely one-off route; for an app-wide look, a custom `PageTransitionsBuilder` in `ThemeData.pageTransitionsTheme` keeps routes on `MaterialPageRoute` and lets you choose per platform. ## Common mistakes - **Creating an `AnimationController` for the route.** The route already owns one and exposes it to you as `animation`; `transitionsBuilder` only reads it. - **Doing expensive work in `transitionsBuilder`.** It runs on every animation frame, so keep it to transition widgets wrapped around `child`, and build the screen in `pageBuilder`. - **Animating the exit with `secondaryAnimation`.** Popping reverses `animation`; `secondaryAnimation` only describes being covered by another route. - **A transparent background with `opaque: true`.** Once the transition completes the routes underneath stop painting, so the see-through area goes blank. - **Forgetting `reverseTransitionDuration`.** A long, expressive entrance usually wants a shorter exit; the two durations are independent.

  • How do you get the same slide-up on a go_router route?
    Return a `CustomTransitionPage` from the `GoRoute`'s `pageBuilder` instead of using `builder`. It takes `child` and a `transitionsBuilder` with the same four-argument signature, and its durations also default to 300 ms. `NoTransitionPage` is the variant with both durations set to zero. It builds a plain `PageRoute`, so it has no iOS swipe-back either.
  • Why does the screen underneath not move when a PageRouteBuilder is pushed over a MaterialPageRoute?
    A `MaterialPageRoute` runs its outgoing transition only when the next route is also a Material-transition route or exposes a `delegatedTransition`. `PageRouteBuilder` is neither by default, so the covered route's `secondaryAnimation` never drives anything visible and it stays still while your route slides over it.
  • When would you set opaque: false on a PageRouteBuilder?
    When the pushed screen is translucent, such as a dimmed match-celebration overlay over the swipe deck. With `opaque: true`, the Navigator stops painting the routes below once the transition completes, so a transparent background would show nothing behind it. `opaque: false` keeps the routes below painted.

A stage wagon: the set (pageBuilder) is built once backstage, and the wagon (transitionsBuilder) is what rolls it on and off; a second cue (secondaryAnimation) moves it aside when the next set arrives in front.

saying these in an interview costs you the question

  • Leaving out transitionsBuilder falls back to the platform's page transition.
  • pageBuilder runs every frame, so the animation belongs there.
  • secondaryAnimation drives the route's own pop animation.
  • A PageRouteBuilder keeps the iOS back swipe just like MaterialPageRoute.
  • You must create and dispose an AnimationController for each PageRouteBuilder.
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

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

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

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