Flutter Animations Overview
Create smooth, performant animations in Flutter using the right approach for each use case. This skill covers complete animation workflow: from choosing between implicit/explicit approaches to implementing complex effects like hero transitions and staggered animations.
Animation Type Decision Tree
Choose the right animation type based on your requirements:
Implicit Animations - Use when:
Animating a single property (color, size, position) Animation is triggered by state change No need for fine-grained control
Explicit Animations - Use when:
Need full control over animation lifecycle Animating multiple properties simultaneously Need to react to animation state changes Creating custom animations or transitions
Hero Animations - Use when:
Sharing an element between two screens Creating shared element transitions User expects element to "fly" between routes
Staggered Animations - Use when:
Multiple animations should run sequentially or overlap Creating ripple effects or sequential reveals Animating list items in sequence
Physics-Based Animations - Use when:
Animations should feel natural/physical Spring-like behavior, scrolling gestures Draggable interactions Implicit Animations
Implicit animations automatically handle the animation when properties change. No controller needed.
Common Implicit Widgets
AnimatedContainer - Animates multiple properties (size, color, decoration, padding):
AnimatedContainer( duration: const Duration(milliseconds: 300), curve: Curves.easeInOut, width: _expanded ? 200 : 100, height: _expanded ? 200 : 100, color: _expanded ? Colors.blue : Colors.red, child: const FlutterLogo(), )
AnimatedOpacity - Simple fade animation:
AnimatedOpacity( opacity: _visible ? 1.0 : 0.0, duration: const Duration(milliseconds: 300), child: const Text('Hello'), )
TweenAnimationBuilder - Custom tween animation without boilerplate:
TweenAnimationBuilder
Other implicit widgets:
AnimatedPadding - Padding animation AnimatedPositioned - Position animation (in Stack) AnimatedAlign - Alignment animation AnimatedContainer - Multiple properties AnimatedSwitcher - Cross-fade between widgets AnimatedDefaultTextStyle - Text style animation Best Practices Prefer implicit animations for simple cases Use appropriate curves for natural motion (see Curves class) Set curve and duration for predictable behavior Use onEnd callback when needed Avoid nested implicit animations for performance Explicit Animations
Explicit animations provide full control with AnimationController.
Core Components
AnimationController - Drives the animation:
late AnimationController _controller;
@override void initState() { super.initState(); _controller = AnimationController( duration: const Duration(seconds: 2), vsync: this, ); }
@override void dispose() { _controller.dispose(); super.dispose(); }
Tween - Interpolates between begin and end values:
animation = Tween
CurvedAnimation - Applies a curve to the animation:
animation = CurvedAnimation( parent: _controller, curve: Curves.easeInOut, );
AnimatedWidget Pattern
Best for reusable animated widgets:
class AnimatedLogo extends AnimatedWidget {
const AnimatedLogo({super.key, required Animation
@override
Widget build(BuildContext context) {
final animation = listenable as Animation
AnimatedBuilder Pattern
Best for complex widgets with animations:
class GrowTransition extends StatelessWidget { const GrowTransition({ required this.child, required this.animation, super.key, });
final Widget child;
final Animation
@override Widget build(BuildContext context) { return Center( child: AnimatedBuilder( animation: animation, builder: (context, child) { return SizedBox( height: animation.value, width: animation.value, child: child, ); }, child: child, ), ); } }
Monitoring Animation State animation.addStatusListener((status) { switch (status) { case AnimationStatus.completed: _controller.reverse(); break; case AnimationStatus.dismissed: _controller.forward(); break; default: break; } });
Multiple Simultaneous Animations
class AnimatedLogo extends AnimatedWidget {
const AnimatedLogo({super.key, required Animation
static final _opacityTween = Tween
@override
Widget build(BuildContext context) {
final animation = listenable as Animation
Built-in Explicit Transitions
Flutter provides ready-to-use transitions:
FadeTransition - Fade animation ScaleTransition - Scale animation SlideTransition - Slide animation SizeTransition - Size animation RotationTransition - Rotation animation PositionedTransition - Position animation (in Stack)
Example:
FadeTransition( opacity: _animation, child: const FlutterLogo(), )
Performance Tips Dispose controllers when widget is removed Use AnimatedBuilder for optimal rebuilds Avoid setState() in animation listeners (use AnimatedWidget/AnimatedBuilder) Use timeDilation to slow animations during debugging Hero Animations
Hero animations create shared element transitions between screens.
Basic Hero Animation
Source screen:
Hero( tag: 'hero-image', child: Image.asset('images/logo.png'), )
Destination screen:
Hero( tag: 'hero-image', // Same tag! child: Image.asset('images/logo.png'), )
Complete Example class PhotoHero extends StatelessWidget { const PhotoHero({ super.key, required this.photo, this.onTap, required this.width, });
final String photo; final VoidCallback? onTap; final double width;
@override Widget build(BuildContext context) { return SizedBox( width: width, child: Hero( tag: photo, child: Material( color: Colors.transparent, child: InkWell( onTap: onTap, child: Image.asset(photo, fit: BoxFit.contain), ), ), ), ); } }
Navigating between screens:
Navigator.of(context).push(
MaterialPageRoute
Radial Hero Animation
Transform from circle to rectangle during transition:
class RadialExpansion extends StatelessWidget { const RadialExpansion({ super.key, required this.maxRadius, this.child, }) : clipRectSize = 2.0 * (maxRadius / math.sqrt2);
final double maxRadius; final double clipRectSize; final Widget? child;
@override Widget build(BuildContext context) { return ClipOval( child: Center( child: SizedBox( width: clipRectSize, height: clipRectSize, child: ClipRect(child: child), ), ), ); } }
Use with MaterialRectCenterArcTween for center-based interpolation:
static RectTween _createRectTween(Rect? begin, Rect? end) { return MaterialRectCenterArcTween(begin: begin, end: end); }
Hero Best Practices Use unique, consistent tags (often the data object itself) Keep hero widget trees similar between routes Wrap images in Material with transparent color for "pop" effect Use timeDilation to debug transitions Consider HeroMode to disable hero animations when needed Staggered Animations
Run multiple animations with different timing.
Basic Staggered Animation
All animations share one controller:
class StaggerAnimation extends StatelessWidget {
StaggerAnimation({super.key, required this.controller})
: opacity = Tween
final AnimationController controller;
final Animation
Widget _buildAnimation(BuildContext context, Widget? child) { return Container( alignment: Alignment.bottomCenter, child: Opacity( opacity: opacity.value, child: Container( width: width.value, height: 150, color: Colors.blue, ), ), ); }
@override Widget build(BuildContext context) { return AnimatedBuilder( animation: controller, builder: _buildAnimation, ); } }
Interval-Based Timing
Each animation has an Interval between 0.0 and 1.0:
animation = Tween
Common Tweens borderRadius = BorderRadiusTween( begin: BorderRadius.circular(4), end: BorderRadius.circular(75), ).animate( CurvedAnimation( parent: controller, curve: const Interval(0.375, 0.500, curve: Curves.ease), ), );
Staggered Menu Animation class _MenuState extends State