Getting Started With Staggered Animations in Flutter
Animations in mobile apps are powerful tools to attract users’ attention. They make transitions between screens and states smoother and more appealing for the user. In this tutorial, you’ll learn how to implement animations in Flutter. By Sébastien Bel.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
Getting Started With Staggered Animations in Flutter
30 mins
- Getting Started
- Animating Goodnightmoonhellosun
- Using Implicit Animations
- Applying Tween Animations
- Implementing Curve Animations
- Animating the Sun and the Moon Implicitly
- Using Explicit Animations
- Animating With Animation
- AnimatedBuilder
- Coordinating Animations With Intervals
- Introducing AnimatedWidgets
- Implementing AnimatedWidget
- Animating Daytime and Nighttime Transition
- Animating the Theme
- Animating the TodayDetails Widget
- Interpolating a Custom Object
- Adding More Animations
- Add an Animation to BottomCard
- Make a Startup Animation
- Improve CloudyWidget
- Make It Rain or Snow
- Solution
- Where to Go From Here?
Introducing AnimatedWidgets
Adding many AnimatedBuilders to your build() can make things seem a bit messy. Instead, you can use AnimatedWidget to separate the different parts of your UI.
Make the following changes to SunWidget:
// 1
class SunWidget extends AnimatedWidget {
// 2
const SunWidget({Key? key, required Animation<Offset> listenable})
: super(key: key, listenable: listenable);
// 3
Animation<Offset> get _animation => listenable as Animation<Offset>;
@override
Widget build(BuildContext context) {
return LayoutBuilder(builder: (context, constraints) {
final minSize = min(constraints.maxWidth, constraints.maxHeight);
final sunSize = minSize / 2.0;
return Transform.translate(
// 4
offset: _animation.value,
child: Container(
width: sunSize,
height: sunSize,
decoration: BoxDecoration(
// …
),
),
);
});
}
}
In the code above, you:
- Extend
AnimatedWidgetinstead ofStatelessWidget. - Give
superconstructor yourListenable(Animation). - Cast
ListenableasAnimationfor later use. - Use the current animation value in your
build()method. It triggers each timeListenableupdates.
Now, do the same for MoonWidget:
class MoonWidget extends AnimatedWidget {
const MoonWidget({Key? key, required Animation<Offset> listenable})
: super(key: key, listenable: listenable);
Animation<Offset> get _animation => listenable as Animation<Offset>;
@override
Widget build(BuildContext context) {
return LayoutBuilder(builder: (context, constraints) {
final minSize = min(constraints.maxWidth, constraints.maxHeight);
final moonSize = minSize / 2.0;
return Transform.translate(
offset: _animation.value,
child: Container(
// Rest of MoonWidget
),
);
});
}
}
MoonWidget works in the same way as SunWidget, but the Transform.translate child changes.
Finally, replace _sunOrMoon() in home_page.dart with the following:
return Stack(
children: [
SunWidget(listenable: _sunMoveAnim),
MoonWidget(listenable: _moonMoveAnim),
],
);
Instead of using AnimatedBuilders, you directly use your AnimatedWidgets.
Hot reload and launch the animation.

The animation looks the same, but it’s different below the hood. :]
Unlike AnimatedBuilder, AnimatedWidget doesn’t have a child property to optimize its build(). However, you could add it manually by adding an extra child property, as in this example class:
class AnimatedTranslateWidget extends AnimatedWidget {
const AnimatedTranslateWidget(
{Key? key,
required Animation<Offset> translateAnim,
required Widget child})
: _child = child,
super(key: key, listenable: translateAnim);
// Child optimization
final Widget _child;
Animation<Offset> get animation => listenable as Animation<Offset>;
@override
Widget build(BuildContext context) {
return Transform.translate(
offset: animation.value,
child: _child,
);
}
}
The resulting class is an animated version of Transform.translate(), just like SlideTransition. You use it with your non-animated widgets as argument.
There’s a full list of widgets that have an AnimatedWidget version. You name them according to the format FooTransition, where Foo is the animation’s name. You can use them and still control your animations because you pass them Animation objects.
Implementing AnimatedWidget
If the animation you want to achieve is not very heavy and it’s OK for you to skip child optimization, you may make your widget implement AnimatedWidget. Here, you can use the animated versions of SunWidget and MoonWidget without it, for instance.
However, test it on your low-end target devices to be sure it runs well before putting it in production.
You may also want to achieve a special kind of animation that doesn’t already exist in the framework. In this case, you could create a widget implementing AnimatedWidget to animate the components of your app. For instance, you could combine a fade effect with a translate effect and create a SlideAndFadeTransition widget.
Animating Daytime and Nighttime Transition
Now that you know the theory, you can apply it to make your day/night transition! You already have animations for the sun and the moon, but they don’t depend on the current theme: The sun will always leave and the moon will always enter.
Start by updating _initThemeAnims() to change that:
void _initThemeAnims({required bool dayToNight}) {
final disappearAnim =
Tween<Offset>(begin: const Offset(0, 0), end: Offset(-widget.width, 0))
.animate(CurvedAnimation(
parent: _animationController,
curve: const Interval(
0.0,
0.3,
curve: Curves.ease,
),
));
final appearAnim =
Tween<Offset>(begin: Offset(widget.width, 0), end: const Offset(0, 0))
.animate(CurvedAnimation(
parent: _animationController,
curve: const Interval(
0.7,
1.0,
curve: Curves.ease,
),
));
_sunMoveAnim = dayToNight ? disappearAnim : appearAnim;
_moonMoveAnim = dayToNight ? appearAnim : disappearAnim;
}
Instead of using raw values for the offset, you used widget.width here to make sure you have a good animation, no matter what the screen size is. Also, instead of directly defining the sun and moon animations, you set appearAnim and disappearAnim. Then, you assign them to _sunMoveAnim and _moonMoveAnim depending on the animation you need to perform — day to night or night to day.
Intervals don’t follow each other because you’ll add more animations between them.
Next, replace the contents of _sunOrMoon() with this code:
if (_isDayTheme) {
return SunWidget(listenable: _sunMoveAnim);
} else {
return MoonWidget(listenable: _moonMoveAnim);
}
You return only one widget, depending on the current theme.
Now, you need to be able to change the theme. You’ll listen to _animationController for this.
Update switchTheme() and add the necessary methods below:
void _switchTheme() {
// 1
if (_isDayTheme) {
_animationController.removeListener(_nightToDayAnimListener);
_animationController.addListener(_dayToNightAnimListener);
} else {
_animationController.removeListener(_dayToNightAnimListener);
_animationController.addListener(_nightToDayAnimListener);
}
// 2
_initThemeAnims(dayToNight: _isDayTheme);
// 3
setState(() {
_animationController.reset();
_animationController.forward();
});
}
void _dayToNightAnimListener() {
_animListener(true);
}
void _nightToDayAnimListener() {
_animListener(false);
}
void _animListener(bool dayToNight) {
// 4
if ((_isDayTheme && dayToNight || !_isDayTheme && !dayToNight) &&
_animationController.value >= 0.5) {
setState(() {
_isDayTheme = !dayToNight;
});
}
}
Here’s what’s happening above:
- Remove the previous listener before adding the new one.
- Init again
Animationobjects with the new_isDayThemesetting. - Refresh state with new
Animationobjects, then launch the animation from the start. - In the listener, eventually update
_isDayThemebased on the current animation value.
Hot reload and click SWITCH THEMES.

Animating the Theme
You can animate the theme transition as well. Start by declaring the following Animation below _moonMoveAnim:
late Animation<ThemeData> _themeAnim;
Next, init it at the end of _initThemeAnims():
_themeAnim = (dayToNight
? ThemeDataTween(begin: _dayTheme, end: _nightTheme)
: ThemeDataTween(begin: _nightTheme, end: _dayTheme))
.animate(
CurvedAnimation(
parent: _animationController,
curve: const Interval(
0.3,
0.7,
curve: Curves.easeIn,
),
),
);
The code above interpolates between two ThemeDatas with ThemeDataTweens. It’s another example of objects that need a dedicated Tween class.
Finally, replace the contents of build():
return AnimatedBuilder(
animation: _themeAnim,
child: _content(),
builder: (context, child) {
return Theme(
data: _themeAnim.value,
child: Builder(
builder: (BuildContext otherContext) {
return child!;
},
),
);
},
);
AnimatedBuilder updates the Theme of your HomePage based on _themeAnim‘s value.
Hot restart and launch the animation.

Now, Theme‘s colors change progressively thanks to the animation.