Flutter animations: from basics to production patterns
By Ann Tech · 12 April 2026
Flutter's animation system is layered — simple cases need almost no code, complex cases give you full control. The key is knowing which layer to use.
Layer 1: Implicit animations (simplest)
When a value changes, animate to the new value automatically:
AnimatedContainer(
duration: const Duration(milliseconds: 300),
curve: Curves.easeInOut,
width: _isExpanded ? 200 : 100,
height: _isExpanded ? 200 : 100,
color: _isExpanded ? Colors.blue : Colors.grey,
child: child,
)
AnimatedOpacity(
duration: const Duration(milliseconds: 200),
opacity: _isVisible ? 1.0 : 0.0,
child: child,
)
AnimatedSwitcher(
duration: const Duration(milliseconds: 300),
child: _showA ? const WidgetA(key: Key('a')) : const WidgetB(key: Key('b')),
)
Use implicit animations for: hover effects, show/hide transitions, value changes triggered by user actions.
Layer 2: TweenAnimationBuilder
Animate any value that doesn't have a built-in Animated* widget:
TweenAnimationBuilder<double>(
tween: Tween(begin: 0, end: _progress),
duration: const Duration(milliseconds: 600),
curve: Curves.easeOut,
builder: (context, value, child) {
return CircularProgressIndicator(value: value);
},
)
Layer 3: Explicit AnimationController
For looping, reversing, chaining, or precise timing control:
class _PulseButtonState extends State<PulseButton>
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);
_scale = Tween<double>(begin: 1.0, end: 1.1).animate(
CurvedAnimation(parent: _controller, curve: Curves.easeInOut),
);
}
@override
Widget build(BuildContext context) {
return ScaleTransition(
scale: _scale,
child: widget.child,
);
}
@override
void dispose() {
_controller.dispose(); // Always dispose
super.dispose();
}
}
Staggered animations
Animate multiple elements with offset delays:
class _StaggeredListState extends State<StaggeredList>
with SingleTickerProviderStateMixin {
late final AnimationController _controller;
@override
void initState() {
super.initState();
_controller = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 1200),
)..forward();
}
Animation<double> _itemAnimation(int index) {
final start = index * 0.1; // Each item starts 10% later
final end = start + 0.6;
return CurvedAnimation(
parent: _controller,
curve: Interval(start.clamp(0.0, 1.0), end.clamp(0.0, 1.0),
curve: Curves.easeOut),
);
}
@override
Widget build(BuildContext context) {
return Column(
children: widget.items.asMap().entries.map((e) {
return FadeTransition(
opacity: _itemAnimation(e.key),
child: SlideTransition(
position: Tween<Offset>(begin: const Offset(0, 0.3), end: Offset.zero)
.animate(_itemAnimation(e.key)),
child: e.value,
),
);
}).toList(),
);
}
}
Hero animations
Shared element transitions between screens:
// Source screen
Hero(
tag: 'product-image-${product.id}',
child: CachedNetworkImage(imageUrl: product.imageUrl),
)
// Destination screen
Hero(
tag: 'product-image-${product.id}',
child: CachedNetworkImage(imageUrl: product.imageUrl, fit: BoxFit.cover),
)
Flutter handles the transition automatically. The tag must match exactly.
Performance tips
- Use
constfor animation targets when possible - Wrap animating subtrees in
RepaintBoundaryto isolate repainting - Use
Transformwidgets (ScaleTransition, SlideTransition) instead of rebuilding layout — they run on the raster thread - Profile in
flutter run --profilemode —flutter run --debughides real performance issues
Common pitfalls
Not disposing AnimationController. Every controller allocates a ticker that runs forever. Always call _controller.dispose() in dispose(). Forgetting this causes memory leaks and "animation after dispose" exceptions.
Animating layout properties unnecessarily. Animating width and height triggers layout on every frame. Prefer Transform.scale (no layout) over AnimatedContainer(width:) for performance-sensitive animations.
Using setState for animation values. Calling setState on every animation tick rebuilds the entire widget subtree. Use AnimatedBuilder or *Transition widgets which only rebuild what's necessary.
Sign in to like, dislike, or report.