Flutter app development tutorials by Andrea Bizzotto

Learn Flutter Animations in 8 Minutes + Free Gallery App on GitHub

Animations are a great way to spice up your Flutter apps and delight your users.

So let's learn about them! We'll cover:

  • Implicitly Animated Widgets
  • Tweens & TweenAnimationBuilder
  • AnimationController & AnimatedBuilder
  • Built-in Explicit Transition Widgets

These are just the basics. So I'm also sharing the source code for a new gallery app. This contains many examples of animations that you can learn from and use in your projects:

Flutter Animations Gallery - Web Demo. Open in a separate window

Ready? Let's get started!

Implicitly Animated Widgets

Flutter ships with a bunch of so-called implicitly animated widgets that you just drop in your code to easily add animations.

For example, let's take a look at AnimatedContainer.

AnimatedContainer

Here's a very boring Container with a given width, height, and color.

Container( width: 200, height: 200, color: Colors.red, )

Let's replace it with AnimatedContainer and give it a duration:

AnimatedContainer( width: 200, height: 200, color: Colors.red, duration: Duration(milliseconds: 250), )

To animate this, we need to do a few things:

// 1. Convert the parent class to a StatefulWidget class AnimatedContainerPage extends StatefulWidget { @override _AnimatedContainerPageState createState() => _AnimatedContainerPageState(); } class _AnimatedContainerPageState extends State<AnimatedContainerPage> { // 2. declare the container properties as state variables double _width = 200; double _height = 200; Color _color = Colors.red; @override Widget build(BuildContext context) { return Scaffold( body: Center( child: AnimatedContainer( // 3. pass the state variables as arguments width: _width, height: _height, color: _color, duration: Duration(milliseconds: 250), ), ), // 4. add a button with a callback floatingActionButton: FloatingActionButton( child: Icon(Icons.play_arrow), onPressed: _update, ), ); } // 5. Update the state variables to rebuild the widget void _update() { setState(() { _width = 300; _height = 300; _color = Colors.green; }); } }

With the changes above, we can press the button and the container animates to the new values over the given duration:

AnimatedContainer animation

But if we press the button again, nothing happens because the state variables are already set to the updated values.

To make things more interesting, we can modify the _update method to use a random number generator, so that our container animates to a different set of values every time we press the button.

final random = Random(); void _update() { setState(() { _width = random.nextInt(300).toDouble(); _height = random.nextInt(300).toDouble(); _color = Color.fromRGBO( random.nextInt(128), random.nextInt(128), random.nextInt(128), 1, ); }); }

Not good enough? Then we can change the animations curve to modify the rate of change for the animation value:

AnimatedContainer( width: _width, height: _height, color: _color, duration: Duration(milliseconds: 250), // default curve is Curves.linear curve: Curves.easeInOutCubic, )

This way the animation feels a lot more natural.

Flutter comes with a wide range of curves that you can choose from. And if none of the built-in curves work for you, you can even define your own Curve subclasses.

// a custom sine curve that can be passed to any of the implicitly animated widgets class SineCurve extends Curve { final double count; SineCurve({this.count = 1}); @override double transformInternal(double t) { return sin(count * 2 * pi * t) * 0.5 + 0.5; } }

Alongside AnimatedContainer, Flutter ships with many other implicitly animated widgets such as AnimatedAlign, AnimatedOpacity, AnimatedTheme, and many more. Here is the full list.

How do implicitly animated widgets work?

Implicitly animated widgets have one or more animatable properties that can be set to a target value. When the target value changes, the widget animates the property from the old value to the new value over the given duration.

This makes them easy to use, because all you have to do is to update the target value(s), and the widget will take care of the animation(s) under the hood.

However, they can only be used for animations that go forward. If you need an animation that repeats or goes in reverse, you'll need an explicit animation (see below).

Tweens & TweenAnimationBuilder

Tween stands for in-between, and represents a range with a beginning and an end:

Tweens Diagram

We can use tweens to represent animation values within that range.

And Flutter gives us a TweenAnimationBuilder that we can use to define our own custom implicit animations.

Use TweenAnimationBuilder if you need to create a basic animation, but none of the built-in implicit animations widgets (e.g. AnimatedFoo) does what you need.

How does this work?

Well, let's get back to the boring Container:

Container(width: 120, height: 120, color: Colors.red)

We can wrap this with a TweenAnimationBuilder and give it a Duration and a Tween:

TweenAnimationBuilder<double>( // 1. add a Duration duration: Duration(milliseconds: 500), // 2. add a Tween tween: Tween(begin: 0.0, end: 1.0), // 3. add a child (optional) child: Container(width: 120, height: 120, color: Colors.red), // 4. add the buiilder builder: (context, value, child) { // 5. apply some transform to the given child return Transform.translate( offset: Offset(value * 200 - 100, 0), child: child, ); }, )

The child argument is optional and can be used for optimization purposes. For more info, see: Why do TweenAnimationBuilder and AnimatedBuilder have a child argument?

The builder above gives us an animation value within the range specified by the input Tween.

In this example, we use it to translate the child widget by value * 200 - 100 on the X-axis. This will map animation values between (0, 1) to an offset between (-100, 100).

If we put the code above inside a new widget class and hot reload, we can see the animation:

TweenAnimationBuilder translate animation

At this stage, we can extract the Tween's end value to a state variable and use a Slider to update it:

// 1. use a state variable double _value = 0.0; // 2. pass it to the Tween's end value TweenAnimationBuilder<double>( tween: Tween(begin: 0.0, end: _value), ... ) // 3. Add a slider to update the value Slider.adaptive( value: _value, onChanged: (value) => setState(() => _value = value), )

This way, TweenAnimationBuilder will automatically animate to the new value when we interact with the Slider:

TweenAnimationBuilder translation with Slider
Note the delay between the slider and the Container translation. This is because TweenAnimationBuilder animates to the new value over the given duration.

Other types of Tweens

In the example above we've used a Tween of type double.

But there are several built-in Tween subclasses such as ColorTween, SizeTween, and FractionalOffsetTween that you can use to animate between different colors, sizes, and much more.

If you want, you can even define your own Tween subclasses. This is useful if you want to animate between custom objects in your app. See the Tween class documentation for more info on this.

AnimationController

If we want to create explicit animations that can go forward, in reverse, or even repeat forever, we need an AnimationController.

Let's see how to use it:

// 1. Define a StatefulWidget subclass class RotationTransitionPage extends StatefulWidget { const RotationTransitionPage({Key? key}) : super(key: key); @override _RotationTransitionPageState createState() => _RotationTransitionPageState(); } class _RotationTransitionPageState extends State<RotationTransitionPage> // 2. add SingleTickerProviderStateMixin with SingleTickerProviderStateMixin { // 3. create the AnimationController late final _animationController = AnimationController( vsync: this, duration: Duration(milliseconds: 500), ); @override void dispose() { // 4. dispose the AnimationController when no longer needed _animationController.dispose(); super.dispose(); } }

The most interesting line is this:

late final _animationController = AnimationController( vsync: this, duration: Duration(milliseconds: 500), );

By passing this to the vsync argument, we're asking Flutter to produce a new animation value in sync with the screen refresh rate of our device (normally at 60 frames per second). For more info on this, see: Why Flutter animations need a vsync/TickerProvider.

If you have a lot of widgets with explicit animations, setting up an AnimationController every time is quite tedious. This article offers two solutions: How to reduce AnimationController boilerplate code: Flutter Hooks vs extending the State class.

AnimatedBuilder

Now that we have our AnimationController, let's use it to show a rotating Container.

One way to do this is to use an AnimatedBuilder inside the build() method:

@override Widget build(BuildContext context) { return Scaffold( body: Center( // 1. use an AnimatedBuilder child: AnimatedBuilder( // 2. pass our AnimationController as the animation argument animation: _animationController, // 3. pass the child widget that we will animate child: Container(width: 180, height: 180, color: Colors.red), // 4. add a builder argument (this will be called when the animation value changes) builder: (context, child) { // 5. use a Transform widget to apply a rotation return Transform.rotate( // 6. the angle is a function of the AnimationController's value angle: 0.5 * pi * _animationController.value, child: child, ); }, ), ), ); }

However if we run this code, nothing happens. And that's because we forgot to start the animation!

So we can override the initState() method and call forward():

@override void initState() { super.initState(); _animationController.forward(); }

This will "play" the animation once when we load the page (or hot-restart).

If we prefer, we can make the animation repeat forever by calling _animationController.repeat() instead:

Animated Container that rotates forever

How does AnimatedBuilder work?

Let's revisit this again:

AnimatedBuilder( // pass our AnimationController as the animation argument animation: _animationController, // pass the child widget that we will animate child: Container(width: 180, height: 180, color: Colors.red), // add a builder argument builder: (context, child) { // use a Transform widget to apply a rotation return Transform.rotate( // the angle is a function of the AnimationController's value angle: 0.5 * pi * _animationController.value, child: child, ); }, )

AnimatedBuilder takes an argument of type Animation<double>. Since AnimationController extends Animation<double>, we can pass it as an argument.

This will cause the builder to be called every time the animation value changes. We can use this to transform the given child widget, or even return a completely new widget that depends on the animation value.

Built-in explicit transition widgets

AnimationController & AnimationBuilder are very powerful and you can combine them to create some very custom effects.

But sometimes you don't even need an AnimatedBuilder because Flutter already comes with a set of built-in transition widgets that you can use.

For example, we can replace all the code above with a RotationTransition and accomplish the same thing with much less effort:

RotationTransition( turns: _animationController, child: Container(width: 180, height: 180, color: Colors.red), )

Don't like rotations? How about a scale transition?

ScaleTransition( scale: _animationController, child: Container(width: 180, height: 180, color: Colors.red), )

With this change we get a scale animation that repeats indefinitely:

Animated Container that scales forever

AnimationController Listeners

AnimationController can do a lot more than what we've seen so far. For example, we can add a status listener that makes the animation alternate forward and in reverse every time it completes:

@override void initState() { super.initState(); // add a status listener _animationController.addStatusListener((status) { if (status == AnimationStatus.completed) { _animationController.reverse(); } else if (status == AnimationStatus.dismissed) { _animationController.forward(); } }); // start the animation when the widget is first loaded _animationController.forward(); }

CurvedAnimation

Another cool thing we can do is to use a Tween to generate a new Animation object from the parent AnimationController. This is often used to add a custom animation curve:

late final _customAnimation = Tween( begin: 0.0, end: 1.0, ).animate(CurvedAnimation( parent: _animationController, curve: Curves.easeInOut, ));

Once we have this, we can pass it to our transition widget:

ScaleTransition( // use _customAnimation rather than _animationController as an argument scale: _customAnimation, child: Container(width: 180, height: 180, color: Colors.red), )

And by combining the status listener code above with this CurvedAnimation, we get this:

Alternating scale transition

Implicit vs Explicit animation widgets

Now that we've covered all the basics, we should spot some common patterns in how the Flutter animation APIs are named and used:

Implicitly animated widgets

  • they are named AnimatedFoo (AnimatedContainer, AnimatedAlign etc.)
  • they take Duration and Curve arguments
  • they can only animate forward
  • can't find a built-in implicitly animated widget for what you need? Use TweenAnimationBuilder.

Explicit animation widgets

  • they are named FooTransition (RotationTransition, ScaleTransition etc.)
  • they take an Animation argument
  • they can animate forward, in reverse, or repeat forever
  • can't find an explicit animation widget for what you need? Use AnimatedBuilder.

What other patterns do these APIs have in common?

In fact, sometimes it's more convenient to define an AnimatedWidget subclass rather than using AnimatedBuilder.

For example, here is how the RotationTransition widget is implemented inside the Flutter SDK:

class RotationTransition extends AnimatedWidget { const RotationTransition({ Key? key, required Animation<double> turns, this.alignment = Alignment.center, this.child, }) : super(key: key, listenable: turns); // the parent listenable property has type Listenable, // so we use a getter variable to cast it back to Animation<double> Animation<double> get turns => listenable as Animation<double>; final Alignment alignment; final Widget? child; // This build method is called every time the listenable (animation) value changes. // As such, AnimationBuilder is not needed. @override Widget build(BuildContext context) { final double turnsValue = turns.value; final Matrix4 transform = Matrix4.rotationZ(turnsValue * math.pi * 2.0); return Transform( transform: transform, alignment: alignment, child: child, ); } }

In fact, checking the source code for the built-in animation widgets is a great way to learn how to make your own!

Flutter Animations Gallery

There is a lot more to animations than what we have covered. For example:

  • how to create staggered animations?
  • how to use a GestureDetector to drive the animation of completely custom UI widgets?
  • how to do animated theming in Flutter?

To learn more, check out my Flutter Animations Gallery on GitHub, which is a showcase all the most common animation APIs:

Official documentation

The Flutter docs have some extensive documentation, codelabs, and tutorials about the animation APIs. Here is the best place to get started:

Flutter Animations Course

If you're serious about animations and want to learn how to use them in a real-world app, check out my complete Flutter Animations course.

This will teach you how to build a habit-tracking application with completely custom UI and animations. And it includes 7 hours of in-depth content, full source code, extra challenges & much more.

Flutter Animations Course
Flutter Animations Masterclass. View Course

Happy coding!

Want more?

Support my work and fast-track your Flutter learning with my in-depth courses.