Reach for an implicit animation first: give AnimatedContainer, AnimatedOpacity or TweenAnimationBuilder a duration and a new target value, and the widget animates there on its own. Use an explicit AnimationController with Tweens and AnimatedBuilder when you must start, stop, reverse or repeat the motion yourself.
- Implicit animations (ImplicitlyAnimatedWidget subclasses) own their controller: you change a property such as opacity or width inside setState and the widget interpolates from the old value to the new one over `duration`.
- TweenAnimationBuilder is the implicit escape hatch: it animates any value a built-in widget does not cover, by handing the in-between value to a builder.
- Explicit animations use an AnimationController (0.0 to 1.0 over a duration, ticking once per frame) that you create in initState with vsync: this from SingleTickerProviderStateMixin, and dispose in dispose().
- A Tween maps the controller's 0..1 value to the type you need (double, Color, Rect); a CurvedAnimation or curve argument reshapes the timing with one of the Curves constants.
- AnimatedBuilder (or a prebuilt transition such as FadeTransition) rebuilds just the part that depends on animation.value; pass static subtrees through its `child` so they are not rebuilt every frame.
- Hero is the shared-element transition: two Hero widgets with the same tag on two routes, and Navigator push/pop flies the widget between them in an overlay.
| Implicit (ImplicitlyAnimatedWidget) | Explicit (AnimationController) | |
|---|---|---|
| Who owns the controller | The widget, internally | Your State, via vsync: this |
| How it starts | Rebuild with a new target value (usually setState) | controller.forward(), reverse(), repeat(), animateTo(), fling() |
| Control while running | None: it always animates to the latest target | Pause, reverse, repeat, jump to a value, read status |
| Code you write | duration, curve, the new value | Controller, Tween, curve, builder/transition, dispose |
| Typical widgets | AnimatedContainer, AnimatedOpacity, AnimatedPadding, AnimatedPositioned, AnimatedSwitcher, TweenAnimationBuilder | AnimatedBuilder, AnimatedWidget, FadeTransition, SlideTransition, ScaleTransition, SizeTransition |
| Rebuild cost | Rebuilds the implicit widget's subtree each tick | Rebuilds only the builder; static `child` is reused |
| Best for | State-driven UI changes: toggles, selection colour, layout shifts | Loops, gestures, staggered sequences, anything that must be interrupted |
Both end up driving an AnimationController and a Ticker; the difference is who owns the controller and how much control you get.
Thermostat versus a dimmer switch
An implicit animation is a thermostat: you set the target temperature and walk away; the system takes care of the ramp from the current value to the new one, and if you change the target halfway it simply re-aims. An explicit animation is a dimmer switch in your hand: an AnimationController is the knob position (0 to 1), a Tween is the label that says what 0 and 1 mean (dark to bright, small to large), a Curve is how stiff the knob feels through its travel, and AnimatedBuilder is the lamp wired to the knob. You decide when to turn it, how far, and whether to go back.
Minimal working example
import 'package:flutter/material.dart';
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: 300),
curve: Curves.easeInOut,
width: _expanded ? 240 : 120,
height: _expanded ? 160 : 80,
decoration: BoxDecoration(
color: _expanded ? Colors.indigo : Colors.teal,
borderRadius: BorderRadius.circular(_expanded ? 24 : 8),
),
child: const Center(child: Text('Tap me')),
),
);
}
}- L11The only state is the target: which of the two configurations the card should be in.
- L16setState triggers a rebuild; AnimatedContainer notices its properties changed and starts animating toward them.
- L18duration is required for every implicitly animated widget.
- L19curve is optional; Curves.linear is the default. easeInOut, fastOutSlowIn and bounceOut are common choices.
- L20Width, height, decoration colour and border radius are all interpolated together over the same duration.
Use cases
- Reflecting a state change with motion: selected tile colour, expanding a card, showing or hiding a banner (implicit).
- A custom property with no built-in Animated* widget, such as animating a number counting up or a custom painter parameter (TweenAnimationBuilder).
- Continuous or interruptible motion: loading pulses, drag-to-dismiss, staggered entrances, anything that must pause, reverse or repeat (explicit).
- Moving an image or card from a list to a detail page across a route change (Hero).
Benefits
- Implicit widgets need no controller and no dispose; they are often a two-line change to an existing Container or Opacity.
- Explicit animations give frame-accurate control and compose: one controller can drive several Tweens and curves at once.
- Both run on the same Ticker and scheduler, so animations started in the same frame stay in sync.
Limitations
- Implicit widgets always animate to the latest target; you cannot pause them, run them backwards, or know when they finished without extra work (TweenAnimationBuilder has onEnd).
- Explicit animations add boilerplate: mixin, initState, dispose, and careful handling in didUpdateWidget.
- Rebuilding a large subtree each tick (in an implicit widget's child or inside AnimatedBuilder's builder) is the most common animation jank; see the performance topic.
- Hero requires the same tag on exactly one Hero per route and both heroes to be on screen at push/pop time.
Alternatives and trade-offs
- Built-in transition widgets (FadeTransition, SlideTransition, ScaleTransition) — You already have an Animation<double> and want the standard effect without writing a builder.
- AnimatedSwitcher — You replace one child widget with another and want the swap cross-faded; give the children distinct keys.
- Physics simulations (AnimationController.animateWith, fling) — Motion should follow velocity and springs rather than a fixed duration, for example after a drag.
- Page route transitions — The thing that animates is the whole screen; see navigation and routing.
Not disposing the AnimationController
Problem: The ticker keeps firing after the State is gone, costing CPU and raising an assertion about a Ticker that was still active when the widget was disposed.
Fix: Create the controller in initState and call controller.dispose() in dispose(), before super.dispose().
@override
void initState() {
super.initState();
_controller = AnimationController(vsync: this, duration: const Duration(seconds: 1))
..repeat();
}
// no dispose override@override
void dispose() {
_controller.dispose();
super.dispose();
}Rebuilding static widgets inside AnimatedBuilder's builder
Problem: Everything returned from builder is rebuilt on every frame of the animation. A heavy subtree in there (an image, a list, a card with text) is rebuilt 60 to 120 times per second even though it does not change.
Fix: Build the static part once and pass it as AnimatedBuilder's `child`; the builder receives it as its second argument and only wraps it with the animated transform.
AnimatedBuilder(
animation: _controller,
builder: (context, _) => Transform.rotate(
angle: _controller.value * 6.28,
child: const ExpensiveCard(), // rebuilt each frame
),
)AnimatedBuilder(
animation: _controller,
child: const ExpensiveCard(), // built once
builder: (context, child) => Transform.rotate(
angle: _controller.value * 6.28,
child: child,
),
)Calling setState from addListener to drive the whole screen
Problem: The tutorial's first example uses addListener + setState to show the mechanism, but in a real page it marks the entire State's subtree dirty every frame, not just the animated widget.
Fix: Wrap only the animated part in AnimatedBuilder, an AnimatedWidget subclass, or a transition widget such as FadeTransition, so each tick rebuilds the smallest possible subtree.
Hero tags that do not match, or duplicate tags on one route
Problem: If the destination Hero's tag differs from the source, nothing flies and the route just fades in. Two Heroes with the same tag in a single route throw an assertion when a transition starts.
Fix: Use the item's identity (id, URL) as the tag on both routes, and in lists make sure each item's tag is unique.
This page is Boring Doc’s independent explanation. For the complete specification use the official pages below (content © The Flutter Authors, CC BY 3.0; code samples BSD). Dates show when the explanation was last checked and when the official page last changed.
- https://docs.flutter.dev/ui/animations
- Last checked
- Oct 21, 2026
- Source last updated
- 3 weeks ago
- Last synchronized
- 3 weeks ago
- https://docs.flutter.dev/ui/animations/overview
- Last checked
- Oct 21, 2026
- Source last updated
- 3 weeks ago
- Last synchronized
- 3 weeks ago
- https://docs.flutter.dev/ui/animations/tutorial
- Last checked
- Oct 21, 2026
- Source last updated
- 3 weeks ago
- Last synchronized
- 3 weeks ago
- https://docs.flutter.dev/ui/animations/implicit-animations
- Last checked
- Oct 21, 2026
- Source last updated
- 3 weeks ago
- Last synchronized
- 3 weeks ago
- https://docs.flutter.dev/ui/animations/hero-animations
- Last checked
- Oct 21, 2026
- Source last updated
- 3 weeks ago
- Last synchronized
- 3 weeks ago
- https://docs.flutter.dev/cookbook/animation/animated-container
- Last checked
- Oct 21, 2026
- Source last updated
- 3 weeks ago
- Last synchronized
- 3 weeks ago
- https://docs.flutter.dev/cookbook/animation/opacity-animation
- Last checked
- Oct 21, 2026
- Source last updated
- 3 weeks ago
- Last synchronized
- 3 weeks ago
- https://docs.flutter.dev/learn/pathway/tutorial/implicit-animations
- Last checked
- Oct 21, 2026
- Source last updated
- 3 weeks ago
- Last synchronized
- 3 weeks ago
AI_PROVIDER (for example to openai-compatible with a local Ollama or LM Studio endpoint) to enable grounded explanations. Everything else on this page works without an AI provider.