AnimatedBuilder: Rebuilding Efficiently
AnimatedBuilder: Rebuilding Efficiently
When you use an AnimationController directly inside a StatefulWidget, calling setState() on every animation tick rebuilds the entire widget tree rooted at that widget — even parts that have nothing to do with the animation. AnimatedBuilder solves this problem by isolating the rebuild to only the subtree that actually changes, leaving the rest of the widget hierarchy untouched.
AnimatedBuilder is a general-purpose widget that listens to any Listenable (including Animation, AnimationController, and ChangeNotifier) and calls its builder callback on every change. Only the widget subtree returned by builder is rebuilt — the child argument is built once and passed through unchanged.Why Not Just Use setState?
Consider a complex screen with a header, a list, and a small animated icon. If the parent widget owns the AnimationController and listens to it with addListener(() => setState((){}));, Flutter rebuilds the whole screen — header, list, and icon — sixty times per second. This wastes CPU and GPU resources. With AnimatedBuilder, only the icon subtree is rebuilt on each tick.
- Separation of concerns: animation logic lives in the state class; the animated appearance is described in
builder. - Reusable child: heavy, static subtrees are built once and reused across frames via the
childparameter. - Composability:
AnimatedBuildercan wrap any widget, making it easy to animate third-party or legacy widgets without modifying them.
The child Parameter — Your Performance Key
The most important optimisation is passing a pre-built widget as the child argument. Flutter builds this subtree once, caches it, and hands the same instance to every builder call. Without it, every tick would allocate a fresh widget subtree even if that subtree is visually identical across frames.
Basic AnimatedBuilder Pattern
class SpinningLogo extends StatefulWidget {
const SpinningLogo({super.key});
@override
State<SpinningLogo> createState() => _SpinningLogoState();
}
class _SpinningLogoState extends State<SpinningLogo>
with SingleTickerProviderStateMixin {
late final AnimationController _controller;
@override
void initState() {
super.initState();
_controller = AnimationController(
vsync: this,
duration: const Duration(seconds: 2),
)..repeat(); // Loop forever
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
// The Text below is expensive to build — pass it as child
// so it is constructed only once, not on every tick.
return AnimatedBuilder(
animation: _controller,
child: const Text(
'Flutter',
style: TextStyle(fontSize: 32, fontWeight: FontWeight.bold),
),
builder: (BuildContext context, Widget? child) {
// builder is called ~60 times per second.
// Only this Transform is rebuilt; `child` is reused.
return Transform.rotate(
angle: _controller.value * 2 * 3.14159,
child: child, // reused, never rebuilt
);
},
);
}
}
Animating Multiple Properties
A single AnimatedBuilder can read multiple Tween-derived animations driven by the same controller. Chain Tween.animate() calls to create Animation<T> objects, then read their .value inside builder.
Combining Scale, Opacity, and Slide in One Builder
class FancyCard extends StatefulWidget {
final Widget content;
const FancyCard({super.key, required this.content});
@override
State<FancyCard> createState() => _FancyCardState();
}
class _FancyCardState extends State<FancyCard>
with SingleTickerProviderStateMixin {
late final AnimationController _ctrl;
late final Animation<double> _scale;
late final Animation<double> _opacity;
late final Animation<Offset> _slide;
@override
void initState() {
super.initState();
_ctrl = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 600),
);
_scale = Tween<double>(begin: 0.6, end: 1.0).animate(
CurvedAnimation(parent: _ctrl, curve: Curves.easeOutBack),
);
_opacity = Tween<double>(begin: 0.0, end: 1.0).animate(
CurvedAnimation(parent: _ctrl, curve: Curves.easeIn),
);
_slide = Tween<Offset>(
begin: const Offset(0, 0.3),
end: Offset.zero,
).animate(CurvedAnimation(parent: _ctrl, curve: Curves.easeOut));
_ctrl.forward(); // Play once on mount
}
@override
void dispose() {
_ctrl.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: _ctrl,
// widget.content is static — pass it as child so it
// is not reconstructed on every animation frame.
child: widget.content,
builder: (context, child) {
return FadeTransition(
opacity: _opacity,
child: SlideTransition(
position: _slide,
child: ScaleTransition(
scale: _scale,
child: child, // the pre-built content widget
),
),
);
},
);
}
}
FadeTransition, ScaleTransition, SlideTransition, RotationTransition — that are thin wrappers around AnimatedBuilder for single-property animations. Use them when animating a single attribute; reach for AnimatedBuilder directly when you need to read multiple animations in one builder callback or when you want full control over what is rebuilt.Separating Logic from UI with AnimatedBuilder
A clean architectural pattern is to keep all animation controller and tween setup inside the State class, and describe the animated appearance entirely in the AnimatedBuilder.builder function. The parent widget that contains the non-animating content never rebuilds. This keeps responsibilities clear and makes it trivial to unit-test the animations in isolation.
animation.value inside setState() and store it in a member variable. This defeats the purpose of AnimatedBuilder: it rebuilds the whole widget, not just the animated subtree. Always read animation.value directly inside the builder callback.AnimatedBuilder vs AnimatedWidget
AnimatedWidget is the lower-level building block — it is a StatefulWidget subclass that automatically calls setState when the Listenable fires. AnimatedBuilder is a concrete subclass of AnimatedWidget that adds the builder + child convenience. In practice you will almost always use AnimatedBuilder; only extend AnimatedWidget directly when you want to create a reusable, named animated widget class.
Summary
AnimatedBuilder is the idiomatic Flutter way to drive efficient, fine-grained rebuilds from an AnimationController. Pass the stable, expensive portion of the widget tree as child and Flutter will never rebuild it on animation ticks. Use the builder callback to read animation values and return only the lightweight wrapper that changes. This pattern keeps your frame budget healthy and your code clean.
AnimatedBuilder rebuilds only its builder subtree on each tick. Pass static widgets as child so they are built once and reused. Keep all Tween and controller setup in the State class and read .value exclusively inside the builder callback.