Implicit vs Explicit Animations
4 min read
Animations
| Implicit | Explicit | |
|---|---|---|
| You manage | The target value | The controller (start/stop/reverse/loop) |
| Best for | Simple property changes (size, color, padding, fade) | Page transitions, loops, gesture-driven motion, staggered sequences |
| Setup cost | Drop-in widget | TickerProvider + controller + dispose |
| Run on rebuild only | ✅ Yes | ❌ Has its own timeline |
| Reverse / pause | ❌ (target → new target only) | ✅ |
Rule of thumb: if you can express the animation as "value changed, tween to new value," use implicit.
Code in action — implicit
// Tap to expand: AnimatedContainer tweens width, height, color, padding...
class _ExpandState extends State<Expand> {
bool _open = false;
@override
Widget build(BuildContext context) => GestureDetector(
onTap: () => setState(() => _open = !_open),
child: AnimatedContainer(
duration: const Duration(milliseconds: 300),
curve: Curves.easeOutCubic,
width: _open ? 200 : 100,
height: _open ? 200 : 100,
color: _open ? Colors.indigo : Colors.red,
),
);
}
// Common implicit widgets: AnimatedOpacity, AnimatedAlign, AnimatedPadding,
// AnimatedPositioned, AnimatedSwitcher, TweenAnimationBuilder (custom values)
Code in action — explicit, with coordination
class _LogoState extends State<Logo> with SingleTickerProviderStateMixin {
late final _ctrl = AnimationController(
vsync: this,
duration: const Duration(seconds: 2),
);
// Scale during the FIRST half; rotate during the SECOND half
late final _scale = Tween(begin: 1.0, end: 1.5).animate(
CurvedAnimation(parent: _ctrl, curve: const Interval(0.0, 0.5, curve: Curves.easeOut)),
);
late final _rotate = Tween(begin: 0.0, end: 2 * pi).animate(
CurvedAnimation(parent: _ctrl, curve: const Interval(0.5, 1.0, curve: Curves.easeInOut)),
);
@override
void initState() {
super.initState();
_ctrl.repeat();
}
@override
void dispose() { _ctrl.dispose(); super.dispose(); }
@override
Widget build(BuildContext context) => AnimatedBuilder(
animation: _ctrl,
builder: (_, child) => Transform.scale(
scale: _scale.value,
child: Transform.rotate(angle: _rotate.value, child: child),
),
child: const FlutterLogo(size: 100), // child doesn't rebuild — perf win
);
}
When to reach for which
| Goal | Use |
|---|---|
| Toggle a value with a tween | Implicit (AnimatedFoo) |
| Cross-fade between two widgets | AnimatedSwitcher |
| Custom value type (Color, Offset, etc.) | TweenAnimationBuilder |
| Loop forever, reverse on demand | Explicit AnimationController |
| Coordinate multiple properties on one timeline | One controller + multiple CurvedAnimation intervals |
| Drive from gesture (drag, swipe) | Explicit (controller.value = dx / width) |
| Page transitions | PageRouteBuilder with explicit anim controllers |
| Hero animations | Built-in Hero widget (declarative) |
Common mistakes to avoid
// ❌ Reaching for explicit when implicit would do
class _A extends State<X> with SingleTickerProviderStateMixin {
late final _c = AnimationController(vsync: this, ...);
// 30 lines to fade a widget
}
// ✅ AnimatedOpacity(opacity: visible ? 1 : 0, duration: ...)
// ❌ Building inside AnimatedBuilder's builder unnecessarily
AnimatedBuilder(
animation: _ctrl,
builder: (ctx, _) => MyHeavyWidget(value: _ctrl.value), // rebuilt every frame
);
// ✅ Pass the static parts via `child:` and only build the animating part inside builder
// ❌ Forgetting dispose
late final _ctrl = AnimationController(...); // leaks ticker
// ❌ Animating layout-affecting values on a big subtree
AnimatedContainer(...) // triggers layout every frame
// → If only color/transform changes, prefer Transform/Opacity/etc.
// ❌ Hard-coding durations everywhere
Duration: const Duration(milliseconds: 217) // someone's "vibe"
// ✅ Use kThemeAnimationDuration / your design system tokens
Interview follow-ups
-
What does
child:do onAnimatedBuilder? It passes a pre-built widget into the builder as thechildargument. The builder is called every frame, but thechildis built only once — wrap the static parts of your animated subtree inchild:to avoid rebuilding them per frame. Major perf lever. -
When does
AnimatedContainertrigger layout vs paint? Any property that affects size (width,height,padding,margin) makes it tween layout — Flutter does a full layout pass per frame. Color, transform, opacity tween paint only. For big subtrees, preferAnimatedAlign,AnimatedOpacity, orTransform(which is paint-only). -
Implicit animations +
setState— why does the animation start? Implicit widgets remember their last value across rebuilds. OnsetState, the widget rebuilds with a new target value; the implicit widget detects the change and starts a tween from the old to the new value. No controller needed. -
What's the difference between
AnimatedBuilderandAnimatedWidget?AnimatedWidgetis a base class — you subclass it and overridebuild, and it auto-listens to the given animation.AnimatedBuilderis the inline version (lambda-based) — same mechanism, less boilerplate. Functionally equivalent; pick the one that reads better.
How helpful was this content?
Please sign in to rate this article.