Implicit vs Explicit Animations

Medium PriorityAsked in ~65% of mid-level interviews

4 min read

Animations

ImplicitExplicit
You manageThe target valueThe controller (start/stop/reverse/loop)
Best forSimple property changes (size, color, padding, fade)Page transitions, loops, gesture-driven motion, staggered sequences
Setup costDrop-in widgetTickerProvider + 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

GoalUse
Toggle a value with a tweenImplicit (AnimatedFoo)
Cross-fade between two widgetsAnimatedSwitcher
Custom value type (Color, Offset, etc.)TweenAnimationBuilder
Loop forever, reverse on demandExplicit AnimationController
Coordinate multiple properties on one timelineOne controller + multiple CurvedAnimation intervals
Drive from gesture (drag, swipe)Explicit (controller.value = dx / width)
Page transitionsPageRouteBuilder with explicit anim controllers
Hero animationsBuilt-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

  1. What does child: do on AnimatedBuilder? It passes a pre-built widget into the builder as the child argument. The builder is called every frame, but the child is built only once — wrap the static parts of your animated subtree in child: to avoid rebuilding them per frame. Major perf lever.

  2. When does AnimatedContainer trigger 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, prefer AnimatedAlign, AnimatedOpacity, or Transform (which is paint-only).

  3. Implicit animations + setState — why does the animation start? Implicit widgets remember their last value across rebuilds. On setState, 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.

  4. What's the difference between AnimatedBuilder and AnimatedWidget? AnimatedWidget is a base class — you subclass it and override build, and it auto-listens to the given animation. AnimatedBuilder is 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.