skip to content

In Flutter, how does AnimatedContainer animate a property change, and when do you need an AnimationController instead of an implicit widget?

level: juniorimportance: must knowfreq 65%

answer

  1. rebuild with a new value
  2. the State owns a hidden controller
  3. begins from the current value
  4. duration required, curve defaults linear
  5. loops, gestures, on-demand play

basics

~20 s

AnimatedContainer's State keeps its own AnimationController; when a rebuild passes a new property value it tweens from the current value to it over duration and curve. Loops, on-demand playback, gestures and shared timelines need an explicit controller.

solid answer

~40 s

`AnimatedContainer` is an `ImplicitlyAnimatedWidget`: its `State` owns a private `AnimationController` and a tween per animatable property. You change a value (say `height` or `color`) with `setState`; on the rebuild, `didUpdateWidget` sees a target that differs from the tween's end, sets the tween's begin to the value currently on screen, its end to the new target, and runs the controller from 0 over `duration` with `curve` (default `Curves.linear`). `onEnd` fires when it completes. That covers "animate from old to new when state changes". You need your own `AnimationController` when the motion is not a response to a new value: repeating or looping, starting, stopping or reversing on command, following a drag, or several elements sharing one timeline for a staggered sequence.

code

dart · 32 lines
dart
import 'package:flutter/material.dart';

class FaqHeader extends StatefulWidget {
  const FaqHeader({super.key, required this.question});

  final String question;

  @override
  State<FaqHeader> createState() => _FaqHeaderState();
}

class _FaqHeaderState extends State<FaqHeader> {
  bool _open = false;

  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      onTap: () => setState(() => _open = !_open),
      child: AnimatedContainer(
        duration: const Duration(milliseconds: 250),
        curve: Curves.easeOut,
        padding: EdgeInsets.all(_open ? 20 : 12),
        decoration: BoxDecoration(
          color: _open ? Colors.blue.shade50 : Colors.white,
          borderRadius: BorderRadius.circular(_open ? 16 : 8),
        ),
        onEnd: () => debugPrint('FAQ card settled: open=$_open'),
        child: Text(widget.question),
      ),
    );
  }
}

go deeper

for a junior

Recall that you only change a value with setState and give a duration and curve; the widget animates by itself and needs no dispose.

for a middle

Explain the hidden controller and tweens, the begin-from-current-value rule, and why AnimatedOpacity avoids per-frame rebuilds.

for a senior

Choose implicit or explicit on purpose: loops, gesture scrubbing and staggered timelines need a controller; one-off state changes do not.

for a principal

Set a team default of implicit widgets for state-driven motion and reserve controllers for choreography, which keeps lifecycle code rare.

## What an implicit animation is An **implicit animation** is a widget that animates **by itself** whenever one of its properties changes. You never create, start or dispose an animation object: you rebuild the widget with a new value and it moves there over a **`duration`** (required) using a **`curve`** (defaults to `Curves.linear`). Flutter ships a family of them, all subclasses of **`ImplicitlyAnimatedWidget`**: | Widget | Animates | Note | |---|---|---| | `AnimatedContainer` | size, padding, margin, decoration or colour, alignment, transform | `color` and `decoration` cannot both be set | | `AnimatedOpacity` | `opacity` | builds a `FadeTransition`, so it does not rebuild the child each frame | | `AnimatedPadding` | `padding` | a lighter subset of `AnimatedContainer` | | `AnimatedAlign` | `alignment`, plus `widthFactor` / `heightFactor` | | | `AnimatedPositionedDirectional` | `start`, `top`, `end`, `bottom`, `width`, `height` | must be a child of a `Stack`; respects text direction | | `TweenAnimationBuilder` | any value you describe with a `Tween` | the build-your-own implicit animation | ## How it works under the hood 1. The widget's `State` mixes in `SingleTickerProviderStateMixin` and creates a **private `AnimationController`** with the widget's `duration`. 2. For each animatable property it keeps a **tween**. On the first build the tween starts and ends at the initial value, so nothing moves. 3. When the parent rebuilds the widget with a different value, `didUpdateWidget` compares the new target with the tween's end. If they differ, the tween's **begin becomes the value currently on screen**, its end becomes the new target, and the controller runs `forward(from: 0.0)`. 4. Each tick the state rebuilds (or, for `AnimatedOpacity`, a transition widget repaints) with the interpolated value. 5. When the controller completes, **`onEnd`** is called. Because begin is the current value, changing the target mid-flight is smooth: the animation re-aims from wherever it is and runs a full `duration` again. The state also disposes its own controller, so there is nothing to leak. ## When implicit is enough - A property should move from its old value to a new one **because state changed**: a card grows when selected, a button changes colour when enabled, a banner fades when dismissed. - One widget animates on its own, or several animate independently. - `onEnd` is the only lifecycle hook you need. - Several properties of one widget should move together: `AnimatedContainer` drives every changed property from the same internal controller, so padding, colour and radius finish at the same moment. ## When you need an AnimationController - **Looping or repeating** — a pulsing indicator, a spinning refresh icon. An implicit widget only runs when its target changes. - **Playing on command** without a new target value: replay, pause, stop, reverse, jump to a point. - **Following a gesture** — a drag that scrubs the animation's progress frame by frame. - **One timeline for many elements** — a staggered sequence in which each part occupies an interval of the same controller. - **Listening to status** in detail (forward, reverse, dismissed), not just completion. ## A help-centre example In a help-centre app, an FAQ card highlights when the reader opens it: ```dart AnimatedContainer( duration: const Duration(milliseconds: 250), curve: Curves.easeOut, padding: EdgeInsets.all(isOpen ? 20 : 12), decoration: BoxDecoration( color: isOpen ? Colors.blue.shade50 : Colors.white, borderRadius: BorderRadius.circular(isOpen ? 16 : 8), ), child: Text(question), ) ``` Tapping the card flips `isOpen` in `setState`; the padding, colour and corner radius all tween together. The question text itself does not animate — an implicit widget animates **its own properties**, not its child's contents. ## Common mistakes - **Expecting a first-build animation.** An implicit widget starts at its initial value; nothing plays until a later rebuild changes it. (`TweenAnimationBuilder` is the exception: it animates from `begin` to `end` on its first build.) - **Passing both `color` and `decoration`** to `AnimatedContainer` — an assertion, because `color` is shorthand for `BoxDecoration(color: …)`. - **Assuming opacity 0 removes a widget.** A fully transparent child is still laid out and still receives taps; add an `IgnorePointer` or remove it from the tree after `onEnd`. - **Hand-rolling a controller for a one-off state change** — more code, a `dispose` to remember, and no gain over the implicit widget.

  • What happens if the target value changes again while an AnimatedContainer is still animating?
    The state sets each changed tween's begin to the value currently on screen and its end to the new target, then restarts the controller from 0. The motion re-aims smoothly from where it is, and the new run lasts the full `duration`, not the time left over.
  • Why is AnimatedOpacity cheaper per frame than an AnimatedContainer animating its colour?
    `AnimatedOpacity`'s state builds a `FadeTransition` once and lets the animation drive it, so its child is not rebuilt each tick. `AnimatedContainer`'s state calls `setState` on every tick and rebuilds its `Container` and subtree with interpolated values.
  • How would you make a pulsing 'new reply' badge?
    That is a loop, so an implicit widget is the wrong tool: it only runs when its target changes. Use an `AnimationController` with `repeat(reverse: true)` driving a scale or opacity transition, created in `initState` and disposed in `dispose`.

A thermostat: you only set the target temperature, and the heating moves the room there on its own; if you change the target halfway, it heads for the new one from wherever the room is now.

saying these in an interview costs you the question

  • Every implicit widget needs its own AnimationController passed in.
  • An implicit animation plays automatically the first time it builds.
  • Changing the target mid-flight makes the widget jump to the old end first.
  • AnimatedOpacity at 0 removes the widget from hit testing.
  • Implicit widgets can loop if you give them a long duration.