In Flutter, how do you give one pushed route a custom slide-up transition with PageRouteBuilder, and what do its two animations drive?
answer
- callbacks instead of a PageRoute subclass
- pageBuilder builds, transitionsBuilder wraps child
- animation: this route's own push and pop
- secondaryAnimation: a route covering this one
- 300 ms each way by default
basics
~20 sPush 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 linesimport '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
Recall the two callbacks, which one wraps the child, and that animation runs forward on push and backward on pop.
Explain secondaryAnimation, the cached page versus the per-frame transitionsBuilder, and the 300 ms defaults and opaque flag.
Show you know what the custom route loses: iOS swipe-back, the predictive-back preview and coordinated exit of the screen below.
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.