Flutter Animations — Implicit, Explicit, and Page Transitions
In this tutorial, you will learn about Flutter Animations. We cover key concepts, practical examples, and best practices to help you master this topic.
Flutter animations bring UIs to life with smooth motion, supporting implicit animations for simple property changes, explicit AnimationController for complex sequences, and hero transitions for shared element animations.
What Will You Learn
- Implicit animations: AnimatedContainer, AnimatedOpacity, TweenAnimationBuilder
- Explicit animations with AnimationController and Animation
- Tween and Curve for timing and interpolation
- Staggered animations with multiple controllers
- Page transitions with SlideTransition and FadeTransition
- Hero animations for shared elements
- Custom animated widgets
Why It Matters
Animations improve user experience by providing visual feedback, guiding attention, and explaining state changes. Flutter's animation system runs at 60fps because it operates on the compositing layer, not the build layer. Implicit animations handle simple cases automatically. Explicit animations give full control for complex sequences. Hero animations create polished transitions between screens.
Real-World Use
The DodaTech Flutter app uses implicit animations for expanding course cards, explicit animations for a progress tracker, staggered animations for onboarding screens, and hero animations for transitioning between the course list and detail screens. All animations run at 60fps even on mid-range devices.
Learning Path
flowchart LR A[Flutter Theming] --> B[Flutter Animations\nYou are here] B --> C[Flutter Local Storage] style B fill:#f90,color:#fff
Implicit Animations
Implicit animations interpolate between old and new values automatically:
import 'package:flutter/material.dart';
class ImplicitAnimationExample extends StatefulWidget {
@override
State<ImplicitAnimationExample> createState() => _ImplicitAnimationExampleState();
}
class _ImplicitAnimationExampleState extends State<ImplicitAnimationExample> {
bool _expanded = false;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Implicit Animations')),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
AnimatedContainer(
duration: Duration(milliseconds: 500),
curve: Curves.easeInOut,
width: _expanded ? 300 : 150,
height: _expanded ? 200 : 100,
decoration: BoxDecoration(
color: _expanded ? Colors.blue : Colors.red,
borderRadius: BorderRadius.circular(_expanded ? 20 : 10),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.2),
blurRadius: _expanded ? 20 : 8,
offset: Offset(0, _expanded ? 8 : 4),
),
],
),
child: Center(
child: Text(
_expanded ? 'Expanded' : 'Tap me',
style: TextStyle(color: Colors.white, fontSize: 18),
),
),
),
SizedBox(height: 24),
AnimatedOpacity(
duration: Duration(milliseconds: 300),
opacity: _expanded ? 1.0 : 0.0,
child: Text('This fades in', style: TextStyle(fontSize: 18)),
),
SizedBox(height: 24),
ElevatedButton(
onPressed: () => setState(() => _expanded = !_expanded),
child: Text('Toggle'),
),
],
),
),
);
}
}
AnimatedContainer animates its decoration, size, and position changes. AnimatedOpacity fades widgets in and out. Both use Curves for easing and Duration for timing.
Common Implicit Animation Widgets
Flutter provides several implicit animation widgets:
class ImplicitWidgetShowcase extends StatefulWidget {
@override
State<ImplicitWidgetShowcase> createState() => _ImplicitWidgetShowcaseState();
}
class _ImplicitWidgetShowcaseState extends State<ImplicitWidgetShowcase> {
bool _active = false;
@override
Widget build(BuildContext context) {
return Column(
children: [
// AnimatedPositioned (inside a Stack)
// AnimatedPadding
// AnimatedAlign
// AnimatedDefaultTextStyle
// AnimatedScale
AnimatedScale(
scale: _active ? 1.5 : 1.0,
duration: Duration(milliseconds: 300),
child: Icon(Icons.star, size: 40, color: Colors.amber),
),
SizedBox(height: 16),
// TweenAnimationBuilder for custom tweens
TweenAnimationBuilder<double>(
tween: Tween(begin: 0, end: _active ? 1 : 0),
duration: Duration(seconds: 1),
builder: (context, value, child) {
return Opacity(
opacity: value,
child: Transform.rotate(
angle: value * 6.28,
child: child,
),
);
},
child: Icon(Icons.refresh, size: 40),
),
SizedBox(height: 16),
ElevatedButton(
onPressed: () => setState(() => _active = !_active),
child: Text('Animate'),
),
],
);
}
}
TweenAnimationBuilder animates any value type with a custom Builder. Common implicit widgets: AnimatedPositioned, AnimatedPadding, AnimatedAlign, AnimatedDefaultTextStyle, AnimatedCrossFade, AnimatedSwitcher.
Explicit Animations with AnimationController
Explicit animations give fine-grained control over animation state:
class ExplicitAnimationExample extends StatefulWidget {
@override
State<ExplicitAnimationExample> createState() => _ExplicitAnimationExampleState();
}
class _ExplicitAnimationExampleState extends State<ExplicitAnimationExample>
with SingleTickerProviderStateMixin {
late final AnimationController _controller;
late final Animation<double> _animation;
late final Animation<double> _sizeAnimation;
@override
void initState() {
super.initState();
_controller = AnimationController(
duration: Duration(seconds: 2),
vsync: this,
);
_animation = CurvedAnimation(
parent: _controller,
curve: Curves.easeInOut,
);
_sizeAnimation = Tween<double>(begin: 50, end: 150).animate(
CurvedAnimation(parent: _controller, curve: Curves.bounceOut),
);
_controller.repeat(reverse: true);
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: _animation,
builder: (context, child) {
return Center(
child: Container(
width: _sizeAnimation.value,
height: _sizeAnimation.value,
decoration: BoxDecoration(
color: Color.lerp(Colors.blue, Colors.red, _animation.value),
borderRadius: BorderRadius.circular(_sizeAnimation.value / 2),
),
child: child,
),
);
},
child: Center(
child: Text(
'${(_animation.value * 100).toInt()}%',
style: TextStyle(color: Colors.white, fontSize: 18),
),
),
);
}
}
AnimationController drives the animation. CurvedAnimation applies easing. Tween defines the value range. AnimatedBuilder rebuilds the widget tree on each animation frame. Always dispose the controller.
Staggered Animations
Chain multiple animations in sequence:
class StaggeredAnimation extends StatefulWidget {
@override
State<StaggeredAnimation> createState() => _StaggeredAnimationState();
}
class _StaggeredAnimationState extends State<StaggeredAnimation>
with SingleTickerProviderStateMixin {
late final AnimationController _controller;
late final Animation<double> _fadeIn;
late final Animation<Offset> _slideUp;
late final Animation<double> _scale;
@override
void initState() {
super.initState();
_controller = AnimationController(
duration: Duration(milliseconds: 1500),
vsync: this,
);
_fadeIn = Tween<double>(begin: 0, end: 1).animate(
CurvedAnimation(
parent: _controller,
curve: Interval(0.0, 0.4, curve: Curves.easeIn),
),
);
_slideUp = Tween<Offset>(
begin: Offset(0, 0.5),
end: Offset.zero,
).animate(
CurvedAnimation(
parent: _controller,
curve: Interval(0.2, 0.6, curve: Curves.easeOut),
),
);
_scale = Tween<double>(begin: 0.5, end: 1).animate(
CurvedAnimation(
parent: _controller,
curve: Interval(0.5, 1.0, curve: Curves.elasticOut),
),
);
_controller.forward();
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: _controller,
builder: (context, child) {
return Center(
child: Opacity(
opacity: _fadeIn.value,
child: SlideTransition(
position: AlwaysStoppedAnimation(_slideUp.value),
child: Transform.scale(
scale: _scale.value,
child: Card(
child: Padding(
padding: EdgeInsets.all(32),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.rocket_launch, size: 64, color: Colors.blue),
SizedBox(height: 16),
Text('Staggered', style: TextStyle(fontSize: 24)),
Text('Animation', style: TextStyle(fontSize: 18)),
],
),
),
),
),
),
),
);
},
);
}
}
Interval divides the animation timeline. Each sub-animation starts at a different time. The card fades in first, slides up second, and scales third.
Page Transitions
Customize screen transitions:
class CustomPageTransition extends PageRouteBuilder {
final Widget page;
CustomPageTransition({required this.page})
: super(
pageBuilder: (context, animation, secondaryAnimation) => page,
transitionsBuilder: (context, animation, secondaryAnimation, child) {
// Slide from right
return SlideTransition(
position: Tween<Offset>(
begin: Offset(1, 0),
end: Offset.zero,
).animate(CurvedAnimation(
parent: animation,
curve: Curves.easeInOut,
)),
child: child,
);
},
transitionDuration: Duration(milliseconds: 400),
reverseTransitionDuration: Duration(milliseconds: 300),
);
}
// Usage:
// Navigator.push(context, CustomPageTransition(page: DetailScreen()));
// Using PageTransition for fade + scale:
class FadeScaleTransition extends PageRouteBuilder {
final Widget page;
FadeScaleTransition({required this.page})
: super(
pageBuilder: (_, __, ___) => page,
transitionsBuilder: (_, animation, ___, child) {
return FadeTransition(
opacity: animation,
child: ScaleTransition(
scale: CurvedAnimation(
parent: animation,
curve: Curves.easeInOut,
),
child: child,
),
);
},
);
}
PageRouteBuilder creates custom transitions. transitionsBuilder receives the animation and returns the transition widget. Common transitions: SlideTransition, FadeTransition, ScaleTransition, SizeTransition.
Hero Animations
Hero animations create shared element transitions between screens:
class HeroListScreen extends StatelessWidget {
final List<Map<String, String>> items = List.generate(
10,
(i) => {'title': 'Item $i', 'image': 'https://picsum.photos/200?random=$i'},
);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Hero List')),
body: ListView.builder(
itemCount: items.length,
itemBuilder: (context, index) {
return ListTile(
leading: Hero(
tag: 'image_$index',
child: Image.network(
items[index]['image']!,
width: 50,
height: 50,
fit: BoxFit.cover,
),
),
title: Text(items[index]['title']!),
onTap: () => Navigator.push(
context,
MaterialPageRoute(
builder: (_) => HeroDetailScreen(
imageUrl: items[index]['image']!,
title: items[index]['title']!,
tag: 'image_$index',
),
),
),
);
},
),
);
}
}
class HeroDetailScreen extends StatelessWidget {
final String imageUrl;
final String title;
final String tag;
const HeroDetailScreen({
required this.imageUrl,
required this.title,
required this.tag,
});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text(title)),
body: Center(
child: Hero(
tag: tag,
child: Image.network(imageUrl, width: 300, height: 300, fit: BoxFit.cover),
),
),
);
}
}
Both screens must have a Hero widget with the same tag. Flutter animates the hero from the source position to the destination position during the page transition.
Common Mistakes
Not disposing AnimationController: Undisposed controllers cause memory leaks and unwanted animations. Always dispose in
State.dispose().Using implicit animations for complex sequences: Implicit animations handle single property changes. For sequences or parallel animations, use explicit AnimationController.
Building heavy widget trees inside AnimatedBuilder: The builder runs every frame. Keep it lightweight. Use
childparameter for widgets that do not animate.Forgetting TickerProviderStateMixin: AnimationController requires a
vsyncparameter. UseSingleTickerProviderStateMixinfor one controller orTickerProviderStateMixinfor multiple.Animating layout properties without performance consideration: Animating
width,height, orpositiontriggers layout passes. PreferTransformfor position and scale animations.
Practice Questions
- What is the difference between implicit and explicit animations?
- How does AnimationController work with vsync?
- What is the purpose of Tween and Curve?
- How does Hero identify matching elements across screens?
- Challenge: Build a card-flip animation. The card shows a question on the front. Tapping flips it to reveal the answer on the back. Use AnimationController with a Tween on rotationY (Transform) and an Interval for the text cross-fade.
Mini Project
Build an animated onboarding screen:
- Three onboarding pages with different content
- Page indicator dots with dot animation
- Skip and Next buttons with fade transitions
- Hero animation for the main illustration
- Staggered entry animation for text elements
- Smooth page transitions with SlideTransition
- Final page has a "Get Started" button with scale animation
FAQ
What is Next
Now that you understand animations, learn about local storage. Proceed to Flutter Local Storage for SharedPreferences, SQLite, and file storage. Then explore Flutter Firebase Integration for cloud services.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro