Week 14: Implicit & Explicit Animations

Everything built so far has functioned correctly but appeared and disappeared abruptly. This week is where the app starts to feel considered — Flutter's two animation systems, from the nearly-free implicit widgets to full manual control with AnimationController.

Phase 7 of 8 Week 14 of 20 ~4 Hours Hands-on Exercise Included

By the end of this week, you'll be able to

  • Animate a property change with an implicit animation widget like AnimatedContainer
  • Fade a widget in and out with AnimatedOpacity
  • Build a custom animation with AnimationController and a Tween
  • Choose correctly between an implicit and an explicit animation for a given need

1. Implicit Animations

An implicit animation widget animates automatically, on its own, whenever one of its properties changes between builds — no controller, no explicit start/stop, just a new target value.

AnimatedContainer — animates size, color, and more together
class ExpandingCard extends StatefulWidget {
  const ExpandingCard({super.key});
  @override
  State<ExpandingCard> createState() => _ExpandingCardState();
}

class _ExpandingCardState extends State<ExpandingCard> {
  bool _expanded = false;

  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      onTap: () => setState(() => _expanded = !_expanded),
      child: AnimatedContainer(
        duration: const Duration(milliseconds: 250),
        curve: Curves.easeInOut,
        height: _expanded ? 200 : 80,
        color: _expanded ? Colors.deepPurple : Colors.deepPurple.shade200,
      ),
    );
  }
}

The setState call just changes the target values (height, color) — AnimatedContainer itself handles interpolating smoothly between the old and new values over duration. No manual tweening code required for this class of animation at all.

2. AnimatedOpacity & Friends

a simple fade in/out
AnimatedOpacity(
  opacity: _visible ? 1.0 : 0.0,
  duration: const Duration(milliseconds: 300),
  child: const Text('Now you see me'),
)

The same pattern repeats across Flutter's implicit animation family — AnimatedAlign, AnimatedPadding, AnimatedDefaultTextStyle, and several more — each animates exactly one kind of property change automatically, which covers a large share of real UI polish with almost no code.

3. Explicit Animations with AnimationController

Once an animation needs to run on a loop, be paused and resumed manually, or combine multiple properties on custom timing curves independently, implicit animations run out of road — AnimationController gives full manual control.

a pulsing icon, looping indefinitely
class PulsingIcon extends StatefulWidget {
  const PulsingIcon({super.key});
  @override
  State<PulsingIcon> createState() => _PulsingIconState();
}

class _PulsingIconState extends State<PulsingIcon> with SingleTickerProviderStateMixin {
  late final AnimationController _controller;
  late final Animation<double> _scale;

  @override
  void initState() {
    super.initState();
    _controller = AnimationController(
      vsync: this,
      duration: const Duration(milliseconds: 800),
    )..repeat(reverse: true); // loop forward, then backward, indefinitely

    _scale = Tween<double>(begin: 1.0, end: 1.3).animate(
      CurvedAnimation(parent: _controller, curve: Curves.easeInOut),
    );
  }

  @override
  void dispose() {
    _controller.dispose(); // controllers must be disposed
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return ScaleTransition(scale: _scale, child: const Icon(Icons.favorite));
  }
}

SingleTickerProviderStateMixin supplies the vsync that ties the animation's frame updates to the screen's actual refresh cycle — this is what keeps an off-screen animation from wasting cycles rendering frames nobody sees.

4. Hands-on Exercise

Hands-on

Add real motion to an earlier screen

Apply both implicit and explicit animations to polish a screen from an earlier week.

Requirements:

  1. An AnimatedContainer or similar implicit animation on at least one state change in an earlier screen (e.g. a card expanding, a button changing color on selection).
  2. An AnimatedOpacity fade-in for content that loads asynchronously (pairs naturally with Week 9's loading states).
  3. A custom looping animation built with AnimationController and Tween, used for something genuinely ongoing (a pulsing "live" indicator, a loading spinner variant) — not just a one-shot transition an implicit widget could have handled.
  4. Confirm the AnimationController is disposed correctly by navigating to and away from the screen repeatedly and checking for no memory or performance warnings.
  5. A short comment explaining, for each animation you added, why it was implicit or explicit — tying the choice back to this week's criteria.
Hint

If an AnimationController-based animation appears to stutter or not run at all, confirm vsync: this is actually wired up via SingleTickerProviderStateMixin (or TickerProviderStateMixin for multiple controllers) on the State class — a missing or misconfigured vsync is the most common reason an explicit animation doesn't run smoothly.

5. Knowledge Check

Four quick questions. Expand each to check your answer.

Q1

Why doesn't AnimatedContainer need a Tween or a manual interpolation step the way an explicit animation does?

Implicit animation widgets handle interpolation internally — they compare the previous build's property values to the new ones and automatically tween between them over the given duration. The developer only ever provides target values via setState; the widget itself is responsible for the actual animated transition.

Q2

What specific animation need pushes you from implicit animations to AnimationController?

Anything that isn't just a one-shot response to a property change — looping indefinitely, pausing and resuming on demand, or precisely coordinating multiple animated values on independent custom curves and timing — none of which an implicit widget, which only animates automatically between two states, is built to support.

Q3

What does vsync: this actually provide to an AnimationController, and why does it matter for performance?

It ties the animation's frame callbacks to the actual device screen refresh cycle via a Ticker, and — critically — the mixin providing it also suspends those ticks when the associated widget isn't visible on screen. Without a correctly configured vsync, an animation can keep computing and rendering frames even when nothing is actually displaying it, wasting CPU and battery.

Q4

Why must an AnimationController be disposed in the widget's dispose() method?

It holds a real Ticker resource tied into the rendering pipeline and keeps firing frame callbacks on its own schedule — leaving it undisposed after its owning widget is gone means it continues consuming resources and attempting to drive an animation for a widget tree that no longer exists, a leak with no automatic cleanup unless dispose() is called explicitly.