In Flutter, what does AnimatedBuilder's child parameter save when a loading logo rotates every frame, and how does it work?
answer
- builder runs on every tick
- build it once, pass it in
- same widget instance comes back
- identical widget skips the element update
- only the Transform is recreated
basics
~20 sAnimatedBuilder reruns its builder on every tick. A subtree passed as child is built once by the enclosing build and handed back each time, so only the animated wrapper such as Transform.rotate is recreated per frame while the logo subtree is reused.
solid answer
~50 s`AnimatedBuilder(animation:, builder:, child:)` subscribes to the animation and calls `builder(context, child)` whenever it notifies - once per frame while a controller runs. Anything constructed inside the builder is a new widget each tick, so its elements update and rebuild. If the logo's subtree (icon, padding, semantics label) does not depend on the animation, build it in the enclosing `build` and pass it as `child`: the builder receives the same instance every tick and wraps it, for example in `Transform.rotate(angle: controller.value * 2 * math.pi, child: child)`. When the framework finds the identical widget instance in the same slot, it keeps the element as it is without updating or rebuilding it, so per-frame work shrinks to the `Transform`. The child is still rebuilt whenever the enclosing widget rebuilds - it is reused across ticks, not cached forever.
code
dart · 54 linesimport 'dart:math' as math;
import 'package:flutter/material.dart';
class LoadingLogo extends StatefulWidget {
const LoadingLogo({super.key});
@override
State<LoadingLogo> createState() => _LoadingLogoState();
}
class _LoadingLogoState extends State<LoadingLogo>
with SingleTickerProviderStateMixin {
late final AnimationController _controller;
@override
void initState() {
super.initState();
_controller = AnimationController(
vsync: this,
duration: const Duration(seconds: 2),
)..repeat();
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: _controller,
builder: (BuildContext context, Widget? child) {
return Transform.rotate(
angle: _controller.value * 2 * math.pi,
child: child,
);
},
child: Semantics(
label: 'Loading',
child: Padding(
padding: const EdgeInsets.all(8),
child: Icon(
Icons.autorenew,
size: 48,
color: Theme.of(context).colorScheme.primary,
),
),
),
);
}
}go deeper
Recall that AnimatedBuilder's builder runs every tick and that anything not depending on the animation should be passed in as child.
Explain why an identical widget instance lets the element skip its update, and why the child is reused across ticks but rebuilt when the enclosing build runs.
Spot builders that construct whole subtrees per frame, decide between a FooTransition and AnimatedBuilder, and keep the animated wrapper as small as the effect allows.
Set review conventions for animated widgets so per-frame build work stays proportional to what actually moves, across a codebase with many contributors.
## What AnimatedBuilder does each frame **`AnimatedBuilder`** is a widget that listens to a `Listenable` - usually an `Animation` driven by an `AnimationController` - and rebuilds itself whenever it notifies. Its constructor takes three things: - `animation` - the `Listenable` to listen to; - `builder` - a `TransitionBuilder`, that is `Widget Function(BuildContext context, Widget? child)`; - `child` - an optional, **pre-built** widget handed back to the builder. While a controller runs, it notifies once per frame, so the `builder` runs once per frame too. `AnimatedBuilder` is a thin subclass of `ListenableBuilder`, and its build is literally `builder(context, child)`. ## The cost of building inside the builder Every widget constructed inside `builder` is a **new instance** on every tick. When the framework compares that new widget with the old one at the same position, it finds a different instance of the same type and calls `update` on the existing element, which for a stateless or stateful widget means running its `build` again - and so on down the subtree. For a loading logo that is an icon inside padding inside a semantics wrapper, that is several widgets rebuilt every frame for no visual change: only the rotation angle moves. ## How the child parameter short-circuits the work 1. The enclosing `build` method constructs the logo subtree **once** and passes it as `child`. 2. On each tick, `AnimatedBuilder` calls `builder(context, child)` with that **same instance**. 3. The builder returns a fresh `Transform.rotate` wrapping it. The `Transform` itself is new, so its element updates - that is the part that must change. 4. Below it, `Element.updateChild` sees that the child slot holds a widget **identical** to the one already mounted. It keeps the existing element and does not call `update` at all, so nothing in the logo subtree rebuilds. The framework's own documentation for `AnimatedBuilder` describes this under *Performance optimizations*: using the pre-built child is optional but can improve performance significantly, and is good practice. ## The rotating loading logo ```dart AnimatedBuilder( animation: _controller, // repeating, 2 s per turn builder: (BuildContext context, Widget? child) { return Transform.rotate( angle: _controller.value * 2 * math.pi, child: child, // the same instance every tick ); }, child: Semantics( label: 'Loading', child: Padding( padding: const EdgeInsets.all(8), child: Icon(Icons.autorenew, size: 48, color: Theme.of(context).colorScheme.primary), ), ), ) ``` The icon's color depends on the theme, so this subtree cannot be `const`; the `child` parameter is exactly what keeps it out of the per-frame work. (A subtree that *can* be `const` is already a canonical instance and short-circuits the same way, which is why `child` matters most for subtrees that read `context`, fields or parameters.) ## What child does not do - It does **not** cache the subtree across rebuilds of the enclosing widget: if the parent's `build` runs again after a `setState` or a theme change, a new child instance is built and handed to the builder from then on. - It does **not** insert itself: if the builder ignores the `child` argument, the subtree never appears. - It does **not** help a subtree that genuinely depends on the animation value - that part belongs inside the builder. - It does **not** skip layout and paint of the `Transform`; it removes **build** work below it. ## AnimatedBuilder, a transition widget, or a custom AnimatedWidget | Option | Best when | Child handling | |---|---|---| | `RotationTransition(turns: _controller, child: logo)` | a plain rotation | takes `child` directly | | `AnimatedBuilder` | several properties or custom math inline | `child` parameter, passed to `builder` | | an `AnimatedWidget` subclass | a reusable, named animated widget | declare your own `child` field | For the loading logo, `RotationTransition` is the shortest correct answer; `AnimatedBuilder` with `child` is what interviewers use to check that you understand *why* it is cheap.
- Is the child built only once for the whole lifetime of the widget?No. It is built each time the enclosing `build` method runs - after a `setState` in the parent, a theme change, or a new parent configuration - and then reused across every animation tick until the next such build. The saving is per tick, which is where the frequency lies: once per frame for as long as the controller runs.
- What happens if the builder ignores its child argument?The pre-built subtree never appears. `AnimatedBuilder` only passes `child` to the builder; the builder decides what to return and nothing inserts the child automatically. Usually it also means the author rebuilt the subtree inside the builder instead, which forfeits the saving the parameter exists for.
- When is RotationTransition a better fit than AnimatedBuilder for the loading logo?When the animation is a plain rotation. `RotationTransition(turns: _controller, child: logo)` takes the child directly and maps a value of 1.0 to one full turn, so there is no builder to get wrong. `AnimatedBuilder` earns its place when several properties combine or the transform needs custom math.
saying these in an interview costs you the question
- The child parameter makes the child itself animate along with the builder.
- Everything built inside the builder is cached between ticks automatically.
- The child is built once and never again for the widget's whole lifetime.
- AnimatedBuilder inserts the child on its own even if the builder ignores it.
- AnimatedBuilder only accepts an AnimationController, not other Listenables.