Flutter Animation Fundamentals: From Implicit to Explicit
Master Flutter animations by understanding implicit animations, explicit AnimationController, and when to use each approach. Code examples for real-world scenarios.
Published on • September 15, 2026
AI Assistant

Flutter animations fall into two categories: implicit (automatic) and explicit (controlled). Understanding when to use each is the difference between buttery-smooth UIs and janky, broken transitions.
Implicit animations: change a property, get an animation
Implicit animations animate property changes automatically. No controllers, no ticks — just set the new value:
AnimatedContainer(
duration: Duration(milliseconds: 300),
curve: Curves.easeInOut,
width: _expanded ? 200 : 100,
color: _expanded ? Colors.blue : Colors.red,
child: Center(child: Text('Tap')),
)
When _expanded changes, AnimatedContainer interpolates between old and new values over 300ms.
Common implicit animation widgets
| Widget | Animates |
|---|---|
AnimatedContainer | Size, color, padding, margin, decoration |
AnimatedOpacity | Opacity |
AnimatedPadding | Padding |
AnimatedPositioned | Position (inside Stack) |
AnimatedAlign | Alignment |
AnimatedSwitcher | Cross-fade between children |
AnimatedDefaultTextStyle | Text style |
AnimatedSwitcher(
duration: Duration(milliseconds: 200),
child: _isLoading
? CircularProgressIndicator(key: ValueKey('loading'))
: Icon(Icons.check, key: ValueKey('done')),
)
AnimatedSwitcher cross-fades between children when the key changes.
Explicit animations: AnimationController
For complex, sequenced, or interactive animations, use AnimationController:
class PulseAnimation extends StatefulWidget {
@override
_PulseAnimationState createState() => _PulseAnimationState();
}
class _PulseAnimationState extends State<PulseAnimation>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
late Animation<double> _scaleAnimation;
@override
void initState() {
super.initState();
_controller = AnimationController(
duration: Duration(milliseconds: 600),
vsync: this,
);
_scaleAnimation = Tween<double>(begin: 1.0, end: 1.2).animate(
CurvedAnimation(parent: _controller, curve: Curves.easeInOut),
);
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return ScaleTransition(
scale: _scaleAnimation,
child: GestureDetector(
onTap: () => _controller.forward(from: 0),
child: Container(
width: 100,
height: 100,
color: Colors.blue,
),
),
);
}
}
AnimationController fundamentals
AnimationController drives animations frame by frame:
// Forward: 0.0 → 1.0
_controller.forward();
// Reverse: 1.0 → 0.0
_controller.reverse();
// Repeat: loop forward
_controller.repeat();
// Animate to specific value
_controller.animateTo(0.5);
// Set value directly (no animation)
_controller.value = 0.5;
Curves: controlling animation feel
Curves determine how values interpolate over time:
// Linear: constant speed
Curves.linear
// Ease-in: slow start, fast end
Curves.easeIn
// Ease-out: fast start, slow end
Curves.easeOut
// Bounce: playful overshoot
Curves.bounceOut
// Elastic: spring-like overshoot
Curves.elasticOut
// Custom curve
CurvedAnimation(
parent: _controller,
curve: Interval(0.0, 0.5, curve: Curves.easeIn),
)
Staggered animations
Sequence multiple animations with Interval:
late AnimationController _controller;
late Animation<double> _fadeAnimation;
late Animation<Offset> _slideAnimation;
@override
void initState() {
super.initState();
_controller = AnimationController(
duration: Duration(milliseconds: 1000),
vsync: this,
);
_fadeAnimation = Tween<double>(begin: 0, end: 1).animate(
CurvedAnimation(
parent: _controller,
curve: Interval(0.0, 0.5, curve: Curves.easeIn),
),
);
_slideAnimation = Tween<Offset>(
begin: Offset(0, 0.5),
end: Offset.zero,
).animate(
CurvedAnimation(
parent: _controller,
curve: Interval(0.3, 1.0, curve: Curves.easeOut),
),
);
}
@override
Widget build(BuildContext context) {
return FadeTransition(
opacity: _fadeAnimation,
child: SlideTransition(
position: _slideAnimation,
child: Text('Staggered'),
),
);
}
The text fades in during the first 50% of the animation, then slides up from 30% to 100%.
Hero animations
Hero animations create seamless transitions between screens:
// Screen 1
Hero(
tag: 'product-123',
child: Image.network('https://example.com/product.jpg'),
)
// Screen 2
Hero(
tag: 'product-123',
child: Image.network('https://example.com/product.jpg'),
)
When navigating, Flutter automatically animates the image from its position on Screen 1 to Screen 2.
Implicit vs. explicit: when to use each
| Scenario | Use |
|---|---|
| Simple property change (size, color, opacity) | Implicit |
| Tap-triggered pulse | Explicit |
| Staggered entrance | Explicit |
| Screen transition | Hero + implicit |
| Continuous loop (spinner) | Explicit + repeat |
| Interactive drag | Explicit + gesture |
Performance tips
// 1. Use Transform instead of positioning for GPU-accelerated animation
Transform.scale(
scale: _animation.value,
child: widget,
)
// 2. Avoid animating layout properties (width, height, padding)
// Use Transform.scale instead
// 3. Use RepaintBoundary for complex custom painters
RepaintBoundary(
child: CustomPaint(painter: MyPainter()),
)
// 4. Dispose controllers to prevent memory leaks
@override
void dispose() {
_controller.dispose();
super.dispose();
}
AnimationController with vsync
Always pass this as vsync to AnimationController:
class MyWidget extends StatefulWidget {
@override
_MyWidgetState createState() => _MyWidgetState();
}
class _MyWidgetState extends State<MyWidget>
with SingleTickerProviderStateMixin { // Required for vsync
late AnimationController _controller;
// ...
}
The TickerProviderStateMixin provides vsync callbacks that sync animations with screen refresh rates.
Flutter animations are powerful once you understand the two approaches. Start with implicit animations for simple cases, and reach for AnimationController when you need control over timing, sequencing, or interactivity.