In this tutorial series, we are going to learn all about Flutter Animations, starting from what is and how to use an AnimationController
, then moving to improve our code with AnimatedBuilder
, and finishing by integrating Flutter packages to simplify and reduce the boilerplate in our code (simple_animations and animator).
Flutter Animations is a really wide topic. Animations in general are one of the building blocks that make an application a delight to use and interact with. Albeit Animations can really make an application shine, they are usually neglected or postpone to later phases or releases, mainly because they do not contribute to the core functionality of the app. Because of that, how to implement Animations in your app is usually one of the last concepts that you will learn in Flutter. Let's dive in and add some fancy animations to our app!
What is an animation in Flutter
Generally speaking, animations are attributes of a widget that changes in a certain period of time using a specific curve. The value is updated at each frame (usually 60 or more frames per second) which causes the impression of movement.
Understand Curves
A curve controls how the animation changes the value of the animated object. For instance, by customizing the curve we can make the animation slower at the beginning (easeIn
), slower at the end (easeOut
), or both (easeInOut
).
What does it mean changing the value of the animation? Let's see an example. If we use Transform.scale()
to animate the size of a Container
, what happens is that the scale factor grows frame by frame until it reaches the target value. What a curve does is changing the way the values are updated on each frame.
For instance, if initially the scale value is 1.0 and at the end of the animation it becomes 3.0, with a linear curve the value will change equally at each step, as represented below.
I/flutter ( 4998): 1.0
I/flutter ( 4998): 1.0
I/flutter ( 4998): 1.18603
I/flutter ( 4998): 1.37212
I/flutter ( 4998): 1.5316
I/flutter ( 4998): 1.7124199999999998
I/flutter ( 4998): 1.89187
I/flutter ( 4998): 2.07219
I/flutter ( 4998): 2.2584
I/flutter ( 4998): 2.61752
I/flutter ( 4998): 2.80741
I/flutter ( 4998): 2.98988
I/flutter ( 4998): 3.0
As you can see, the value grows linearly by more or less 0.2 after each print. If we instead use a EaseInExponential
curve, the output will be way different.
I/flutter ( 4998): 1.0
I/flutter ( 4998): 1.0
I/flutter ( 4998): 1.0094702293851878
I/flutter ( 4998): 1.0193529780954123
I/flutter ( 4998): 1.037860722541809
I/flutter ( 4998): 1.054507356164977
I/flutter ( 4998): 1.0776168376207351
I/flutter ( 4998): 1.11944580078125
I/flutter ( 4998): 1.8622788190841675
I/flutter ( 4998): 2.7412984085083005
I/flutter ( 4998): 3.0
The animation starts really slow, almost static, and then reaches 3.0 super fast.
I really suggest checking out the official Flutter curves documentation. It showcases all the predefined curves available in Flutter, together with a simple animation that explains beautifully how the animation works.
Animation Setup
In order to use animations in our app (without any extra package), we need a StatefulWidget
. This widget will contain an AnimationController
object as state. We are going to initialize the AnimationController
in initState()
and we need also to dispose it in dispose()
.
import 'package:flutter/material.dart';
class FlutterAnimations extends StatefulWidget {
@override
_FlutterAnimationsState createState() => _FlutterAnimationsState();
}
class _FlutterAnimationsState extends State<FlutterAnimations>
with SingleTickerProviderStateMixin {
AnimationController _controller;
@override
void initState() {
super.initState();
// Initialize here
}
@override
void dispose() {
super.dispose();
_controller.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: Container(),
);
}
}
Any class that contains an AnimationController
needs to have a TickerProviderStateMixin
.
If we only have one AnimationController, then we should use SingleTickerProviderStateMixin
instead of TickerProviderStateMixin
. We add the mixin using the keyword with
of Dart. (Check out the official Dart documentation to learn more about mixins.)
We initialize the AnimationController
inside initState()
by passing the required vsync
parameter as this
, which references to the class's ticker mixin that we added before. Adding this parameter will make the animation's value automatically update on each frame.
We also pass the duration
parameter, which takes a Duration
object that specifies the duration of the animation (in seconds or milliseconds usually, but we are not limited to those two units).
Finally, we start the animation by calling _controller.forward()
at the end of initState()
.
@override
void initState() {
super.initState();
_controller = AnimationController(
vsync: this,
duration: const Duration(
milliseconds: 1600,
),
);
_controller.forward();
}
setState, Rebuilding the UI
The AnimationController
is itself an Animation
of double, with value that goes from 0.0 to 1.0. We can therefore use it directly if all we need are values between 0 and 1 (like opacity for example). The controller has a value
attribute that contains the value of the animation. We are going to use this value by adding _controller.value
inside our build function to animate our widgets.
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Opacity(
opacity: _controller.value,
child: Container(
height: 50,
width: 50,
color: Colors.blue,
),
),
),
);
}
Although the value itself is changing, the widget does not rebuild because we are not calling setState()
. If we do not call setState()
in a StatefulWidget
, the variable value will change, but the framework will never call the build()
method and the UI will remain the same. In order to make the animation work, we need to call setState()
after each update of the animation value. We add a listener function on the controller using the _controller.addListener()
method, which is called on each tick/frame of the application. Inside the listener, we call setState()
with and empty body.
@override
void initState() {
super.initState();
_controller = AnimationController(
vsync: this,
duration: const Duration(
milliseconds: 1600,
),
);
_controller.addListener(() {
setState(() {});
});
_controller.forward();
}
Now we can launch the application and see our animation in action! We basically created a fade-in animation. The opacity of the container changes from 0 (invisible) to 1 (fully visible) in the time specified by the duration attribute.
You made it, you just created your first animation in Flutter! That is the most basic and crude method to create animations in Flutter. We are basically using only an AnimationController
and its value. We specify a duration, we pass the vsync
attribute from the Ticker mixin, and we call setState()
from the listener.
You can now experiment using this method. Try to change the type of animation, for example modifying the size of the Container or the position.
Note: This is the most basic and naive method to use animations in Flutter. Generally speaking, it is considered bad practice to call setState directly in a response to an AnimationController changing. In the next parts of the series, we are going to learn about better ways to handle animations such as AnimatedWidget
and AnimatedBuilder
. (Thanks to Simon Lightfoot for his suggestion on Twitter)
That's it for the first part of this Flutter animations series. In the next article we are going to learn everything about Tweens
, Curves
, and AnimatedWidgets
!
Thanks for reading.