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<double>(
tween: Tween<double>(begin: 0, end: 1),
duration: const Duration(seconds: 1),
builder: (context, value, child) {
return Opacity(
opacity: value,
child: Transform.scale(
scale: value,
child: child,
),
);
},
child: const FlutterLogo(),
)
Other implicit widgets:
AnimatedPadding- Padding animationAnimatedPositioned- Position animation (in Stack)AnimatedAlign- Alignment animationAnimatedContainer- Multiple propertiesAnimatedSwitcher- Cross-fade between widgetsAnimatedDefaultTextStyle- Text style animation
Best Practices
- Prefer implicit animations for simple cases
- Use appropriate curves for natural motion (see
Curvesclass) - Set
curveanddurationfor predictable behavior - Use
onEndcallback 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<double>(begin: 0, end: 300).animate(_controller);
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<double> animation})
: super(listenable: animation);
@override
Widget build(BuildContext context) {
final animation = listenable as Animation<double>;
return Center(
child: Container(
height: animation.value,
width: animation.value,
child: const FlutterLogo(),
),
);
}
}
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<double> 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<double> animation})
: super(listenable: animation);
static final _opacityTween = Tween<double>(begin: 0.1, end: 1);
static final _sizeTween = Tween<double>(begin: 0, end: 300);
@override
Widget build(BuildContext context) {
final animation = listenable as Animation<double>;
return Center(
child: Opacity(
opacity: _opacityTween.evaluate(animation),
child: Container(
height: _sizeTween.evaluate(animation),
width: _sizeTween.evaluate(animation),
child: const FlutterLogo(),
),
),
);
}
}
Built-in Explicit Transitions
Flutter provides ready-to-use transitions:
FadeTransition- Fade animationScaleTransition- Scale animationSlideTransition- Slide animationSizeTransition- Size animationRotationTransition- Rotation animationPositionedTransition- Position animation (in Stack)
Example:
FadeTransition(
opacity: _animation,
child: const FlutterLogo(),
)
Performance Tips
- Dispose controllers when widget is removed
- Use
AnimatedBuilderfor optimal rebuilds - Avoid
setState()in animation listeners (useAnimatedWidget/AnimatedBuilder) - Use
timeDilationto 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<void>(
builder: (context) {
return Scaffold(
appBar: AppBar(title: const Text('Detail')),
body: Center(
child: PhotoHero(
photo: 'images/logo.png',
width: 300.0,
onTap: () => Navigator.of(context).pop(),
),
),
);
},
),
);
Radial Hero Animation
Transform from circle to rectangle during transition:
class RadialExpansi