In Flutter, how does a Hero animation carry a widget between two routes, and what must the two Hero widgets share?
answer
- same tag on both routes
- both routes must be PageRoutes
- HeroController observes the Navigator
- flies in the Navigator's overlay
- destination hero's child is shown
basics
~20 sThe app's HeroController pairs Hero widgets with equal tags on the outgoing and incoming PageRoutes. During the route transition, the destination hero's child flies in the Navigator's overlay between their bounds while placeholders fill both original slots.
solid answer
~40 sWrap the shared visual on both screens in `Hero(tag: ..., child: ...)` with **equal** tags - usually an id such as `'product-42'`. When a `PageRoute` is pushed or popped, the `HeroController` that `MaterialApp` or `CupertinoApp` installs collects the heroes on both routes, pairs them by tag, and for each pair lifts a *shuttle* into the Navigator's `Overlay`. By default the shuttle is the destination hero's child; it starts at the source hero's global rectangle and ends at the destination's, driven by the route's own transition animation curved with `Hero.curve` (default `Curves.fastOutSlowIn`). While it flies, the originals are replaced by placeholders of the same size. `MaterialApp`'s controller moves the rectangle along an arc (`MaterialRectArcTween`), `CupertinoApp`'s in a straight line. Tags must be unique within each route.
code
dart · 51 linesimport 'package:flutter/material.dart';
class Product {
const Product({required this.id, required this.name, required this.thumbnailUrl});
final String id;
final String name;
final String thumbnailUrl;
}
class ProductTile extends StatelessWidget {
const ProductTile({super.key, required this.product});
final Product product;
@override
Widget build(BuildContext context) {
return InkWell(
onTap: () => Navigator.of(context).push(
MaterialPageRoute<void>(
builder: (BuildContext context) => ProductDetailPage(product: product),
),
),
child: Hero(
tag: 'product-${product.id}',
child: Image.network(product.thumbnailUrl, fit: BoxFit.cover),
),
);
}
}
class ProductDetailPage extends StatelessWidget {
const ProductDetailPage({super.key, required this.product});
final Product product;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text(product.name)),
body: Column(
children: <Widget>[
AspectRatio(
aspectRatio: 4 / 3,
child: Hero(
tag: 'product-${product.id}',
child: Image.network(product.thumbnailUrl, fit: BoxFit.cover),
),
),
],
),
);
}
}go deeper
Recall that both screens wrap the shared element in Hero with an equal tag, and that pushing a MaterialPageRoute is enough to trigger the flight.
Explain the HeroController pairing heroes by tag, the shuttle in the Navigator's overlay, the placeholders, and why only PageRoutes take part.
Design tags from stable ids, keep the destination hero available on the first frame, and choose between arc and linear paths deliberately.
Decide where shared-element transitions belong in an app's navigation model and how tag conventions stay consistent across teams and features.
## What a Hero animation is A **hero animation** is a transition in which one visual element appears to travel from one screen to the next - a furniture store's product thumbnail growing into the large photo at the top of its detail page. In Flutter the element is not actually moved between screens. Two separate widgets exist, one on each route, and the framework draws a temporary third copy that flies between them. The widget that marks a candidate is **`Hero`**. Its required parameters are `tag` (an `Object`) and `child`. ## What the two heroes must share 1. **An equal `tag`.** Tags are compared with `==`, as map keys. A stable value such as `'product-${product.id}'` is safest; an object without value equality only matches the very same instance. 2. **Both routes must be `PageRoute`s.** `MaterialPageRoute`, `CupertinoPageRoute` and `PageRouteBuilder` qualify; dialogs and modal bottom sheets use `PopupRoute`s and never get hero flights. 3. **A `HeroController` observing the Navigator.** `MaterialApp` and `CupertinoApp` provide one for their root Navigator through a `HeroControllerScope`, so in a simple app you never see it. 4. **Unique tags within each route.** Two heroes with the same tag on one route raise an error when a transition starts. For good results the two children should also look essentially the same - changes in size and aspect ratio fly well, changes in layout or composition do not. ## What happens during the flight When a `PageRoute` is pushed, the `HeroController` - a `NavigatorObserver` - is told that the top route changed. It then: - collects every `Hero` on the outgoing and incoming routes and pairs them by tag; - measures each pair's rectangles in global coordinates; - inserts a **shuttle** into the Navigator's **`Overlay`**, above both routes - by default the **destination** hero's `child`, wrapped so that `MediaQuery` padding interpolates; - replaces both original heroes with **placeholders**: an empty `SizedBox` of the hero's size, or, for the source hero of a push, its child kept `Offstage` with tickers muted; - animates the shuttle's rectangle with a `Tween<Rect?>` driven by the route's transition animation, curved by `Hero.curve`; - when the flight ends, removes the shuttle and shows the destination hero in place. Popping runs the same flight backwards. The hero's timing therefore **is** the route transition's timing: a hero never flies longer than the route takes to arrive. ## Material and Cupertino paths | App widget | Controller created by | Path of the rectangle | |---|---|---| | `MaterialApp` | `MaterialApp.createMaterialHeroController()` | `MaterialRectArcTween` - corners follow arcs | | `CupertinoApp` | `CupertinoApp.createCupertinoHeroController()` | linear `RectTween` | A single `Hero` can override the path with `createRectTween`. ## The furniture store example ```dart // Grid tile Hero( tag: 'product-${product.id}', child: Image.network(product.thumbnailUrl, fit: BoxFit.cover), ) // Detail page, built immediately on push Hero( tag: 'product-${product.id}', child: Image.network(product.thumbnailUrl, fit: BoxFit.cover), ) ``` The detail page shows the thumbnail URL it already has, so its `Hero` exists on the first frame of the transition and the flight can be measured. ## Mistakes interviewers listen for - Believing the original widget is physically moved from one route to the other. - Using a fresh object as the tag on each screen and expecting it to match. - Expecting a hero to fly into a dialog or a bottom sheet. - Thinking the flight has its own duration independent of the route transition. ## Why the tag is an Object `Hero.tag` is typed `Object`, not `String`, so any value with sensible equality can serve: a string, an `int` id, an enum value or a record such as `('product', id)`, since records compare by their fields. What matters is that the grid and the detail page compute **equal** values independently. Teams usually hide this behind one small function that both screens call, so a change of convention cannot update one side and forget the other.
- Which widget is shown while the hero is in flight, and can you change it?By default it is the destination hero's `child`, placed in the Navigator's overlay at the source hero's position and animated to the destination's. You can supply a different in-flight widget with `flightShuttleBuilder`; if both heroes provide one, the destination's builder wins.
- Why does MaterialApp's hero follow a curved path while CupertinoApp's goes straight?Each app creates its own `HeroController`. `MaterialApp.createMaterialHeroController()` passes a `createRectTween` that returns a `MaterialRectArcTween`, whose corners follow arcs; `CupertinoApp.createCupertinoHeroController()` passes none, so the controller falls back to a linear `RectTween`. A single `Hero` can override either with its own `createRectTween`.
saying these in an interview costs you the question
- The original widget is moved from the old route into the new one.
- Heroes match by widget type, so the tags can differ.
- A hero can fly into a dialog or a modal bottom sheet.
- The hero flight runs on its own timer, separate from the route transition.
- Any two objects with the same fields match as tags, even without value equality.