In Flutter, what do a Hero's flightShuttleBuilder, placeholderBuilder and createRectTween each customise during a hero flight?
answer
- what flies, what stays, which path
- shuttle gets animation and direction
- destination builder wins
- placeholder defaults to a sized box
- arc tween versus linear tween
basics
~10 sflightShuttleBuilder chooses the widget drawn in flight, placeholderBuilder chooses what stays in each hero's slot while it flies, and createRectTween chooses the path its rectangle follows. Hero.curve, since Flutter 3.44, sets the timing.
solid answer
~40 s`flightShuttleBuilder` receives the flight's `BuildContext`, the flight `Animation<double>`, the `HeroFlightDirection` (push or pop) and both heroes' contexts, and returns the widget drawn in the overlay; if both heroes supply one, the destination's is used. It fixes discontinuities - for example wrapping text in `Material(type: MaterialType.transparency)` so it does not fall back to the debug text style mid-flight, or cross-fading two different children. `placeholderBuilder(context, heroSize, child)` replaces the default empty `SizedBox` left in each hero's slot. `createRectTween(begin, end)` returns the `Tween<Rect?>` for the path, overriding the controller's (`MaterialRectArcTween` under `MaterialApp`, linear under `CupertinoApp`). Since Flutter 3.44, `curve` (default `Curves.fastOutSlowIn`) and `reverseCurve` (default `curve.flipped`) set the easing; the detail page's hero decides both directions.
code
dart · 27 linesimport 'package:flutter/material.dart';
Widget productTitleShuttle(
BuildContext flightContext,
Animation<double> animation,
HeroFlightDirection flightDirection,
BuildContext fromHeroContext,
BuildContext toHeroContext,
) {
final Hero toHero = toHeroContext.widget as Hero;
// The shuttle lives in the Navigator's overlay, outside any Material,
// so give the text a transparent Material to inherit normal styling.
return Material(
type: MaterialType.transparency,
child: toHero.child,
);
}
// On the detail page:
// Hero(
// tag: 'product-title-${product.id}',
// flightShuttleBuilder: productTitleShuttle,
// placeholderBuilder: (context, heroSize, child) =>
// SizedBox(width: heroSize.width, height: heroSize.height),
// curve: Curves.easeInOutCubicEmphasized,
// child: Text(product.name), // inherits DefaultTextStyle, so it needs a Material in flight
// )go deeper
Recall that a Hero can change what flies, what stays behind and which path it takes, through three builder-style parameters.
Explain the shuttle signature, destination precedence, the default SizedBox and Offstage placeholders, and why text can lose its style in the overlay.
Fix mid-flight discontinuities with a shuttle or a transparent Material, pick a rect tween for the effect, and know which hero's curve governs each direction.
Decide which hero customisations a shared component library standardises, so product teams get consistent flights without writing shuttles case by case.
## Three questions every flight answers A **hero flight** in Flutter answers three questions: *what* is drawn while flying, *what* is left behind in each hero's slot, and *which path* the flying rectangle follows. The `Hero` widget exposes one parameter for each - `flightShuttleBuilder`, `placeholderBuilder` and `createRectTween` - plus, since Flutter 3.44, `curve` and `reverseCurve` for the timing. ## flightShuttleBuilder: what flies The **shuttle** is the widget inserted into the Navigator's `Overlay` for the duration of the flight. Its builder has the signature: ```dart Widget Function( BuildContext flightContext, Animation<double> animation, HeroFlightDirection flightDirection, // push or pop BuildContext fromHeroContext, BuildContext toHeroContext, ) ``` Facts worth knowing: - By default the shuttle is the **destination** hero's `child`, wrapped so `MediaQuery` padding interpolates between the two routes. - If **both** heroes provide a builder, the **destination's** takes precedence. - `animation` runs 0 to 1 on a push and 1 to 0 on a pop, so a cross-fade between `fromHeroContext` and `toHeroContext` widgets works in both directions. - A shuttle containing a `GlobalKey` that is also used in the source hero's subtree breaks during a push, because both subtrees are in the tree at once. The most common reason to write one is **inherited styling**. The shuttle is built in the overlay, outside the destination page's `Scaffold` and `Material`. `Text` inside the hero then inherits `MaterialApp`'s fallback text style - large red monospace with a double yellow underline, labelled *consider putting your text in a Material* - for the length of the flight. Wrapping the hero's child, or the shuttle, in `Material(type: MaterialType.transparency)` restores normal text styling. ## placeholderBuilder: what stays behind While a hero is in flight, both originals are hidden so the user sees only the shuttle: 1. By default each slot shows an empty `SizedBox` of the hero's size, so the surrounding layout does not jump. 2. For the **source** hero of a push, the original child stays in the tree but `Offstage`, with `TickerMode` disabled, so its state survives the trip. 3. `placeholderBuilder(context, heroSize, child)` replaces that default - for example, a faint outline of the thumbnail, or a `SizedBox` that avoids a `GlobalKey` clash with a custom shuttle. ## createRectTween: which path The flight interpolates a `Rect` from the source hero's bounds to the destination's using a `Tween<Rect?>`. The default comes from the `HeroController`: | Controller | Default tween | Path | |---|---|---| | `MaterialApp`'s | `MaterialRectArcTween` | corners follow arcs | | `CupertinoApp`'s | linear `RectTween` | straight line | | a bare `HeroController()` | linear `RectTween` | straight line | `Hero.createRectTween` overrides this for one hero; `MaterialRectCenterArcTween` is the usual choice for radial, circle-to-square flights. ## curve and reverseCurve: the timing Since **Flutter 3.44**, `Hero` takes `curve` (default `Curves.fastOutSlowIn`) and `reverseCurve` (default `curve.flipped`). On a push the flight uses the **destination** hero's curves; on a pop it uses the hero on the route being popped. In the furniture store both are the detail page's `Hero`, so that one widget controls the easing both ways. The flight's **duration** is still the route transition's. ## Choosing the right knob - Text or theme glitch mid-flight: `flightShuttleBuilder` or a transparent `Material` in the child. - Two visually different children: `flightShuttleBuilder` cross-fading them. - Layout jumping where the thumbnail was: `placeholderBuilder`. - Wrong trajectory: `createRectTween`. - Wrong feel: `curve` and `reverseCurve`. ## Mistakes interviewers listen for - Expecting `flightShuttleBuilder` to change the flight's duration; the route transition sets it. - Assuming the source hero's shuttle wins when both define one. - Believing text keeps the destination page's styling in flight, when the shuttle actually sits outside that page's `Material`. - Confusing `createRectTween` (the path) with `curve` (the easing). - Using `curve` on a project pinned below Flutter 3.44, where the parameter does not exist.
- Both the grid hero and the detail hero define a flightShuttleBuilder. Which one runs?The destination hero's builder takes precedence when both are supplied. On a push the destination is the detail page's hero; on a pop it is the grid's hero. Keeping a single builder on one side, or the same builder on both, avoids a flight that looks different in each direction.
- Which Hero's curve is used when the user pops back from the detail page?On a pop the flight is curved with the hero on the route being popped - the detail page's hero - using its `reverseCurve`, or `curve.flipped` when that is null. On a push it uses the destination, which is also the detail page's hero. So in a grid-to-detail flow the detail hero sets both directions. These parameters exist since Flutter 3.44.
saying these in an interview costs you the question
- flightShuttleBuilder changes how long the hero flight lasts.
- When both heroes define a shuttle builder, the source hero's builder wins.
- The placeholder left behind is always the original child, fully visible.
- createRectTween sets the flight's easing curve.
- Text flies with the destination page's styling because it is still inside that Scaffold.