How to create animations in Flutter with Redux?
Welcome back! 👋
Today we’ll figure out how to create animations in Flutter when we’re using Redux architecture. If you don’t know what Redux is, I encourage you to read my post with the tutorial about Redux — Flutter + Redux — How to make Shopping List App
If you’re familiar with Redux, you probably know that it may ease your life with your app architecture a lot. It’s great tool for separating your presentation and business logic. But what about the combination of Flutter, Redux and animations?
Let’s go for shopping!
In this article, we’ll use flutter_redux package.
Let’s assume simple case: we have a shopping application with a cart, which looks like this:
We can add and remove products from the list.
If you’d like to see the rest of the code (actions, reducers, etc.) you can check full repository here: pszklarska/flutter_redux_animations_app
What we’d like to achieve is to show current cart value in the bar at the bottom of the screen. To complicate things a little bit, we will show it with the animation like this:
As you can see, when we add some products to the empty cart, we’d like to show bottom bar with slide in animation. And when all products are removed, we’d like it to disappear.
How can we do it?
First — let’s check our cart value 💸
We’ll start firstly with adding a simple bar at the bottom of the screen, showing current cart value. Take a look at the code for this bar:
import 'package:flutter/material.dart';
const CART_BAR_HEIGHT = 48.0;
class CartValueBar extends StatelessWidget {
final double cartValue;
const CartValueBar({Key key, this.cartValue})
: super(key: key);
@override
Widget build(BuildContext context) {
return cartValue != 0
? Container(
height: CART_BAR_HEIGHT,
color: Colors.orange,
child: Row(
children: <Widget>[
Padding(
padding: const EdgeInsets.all(16.0),
child: Text(
'Cart value ${_getCartValue()} zł',
),
),
],
),
)
: Container();
}
String _getCartValue() => cartValue.toStringAsFixed(2);
}
Check full source code for this file on GitHub here.
In this code, we have property cartValue
. Depending on this property, we either show the bottom cart bar with cart value or a single Container
. At this point our screen looks like this:
As you can see, right now we don’t have any fancy animation (actually we don’t have any animation at all). To make it work, we need to make a few changes in our widget.
Show me some nice animation!
First one is to convert our CartValueBar
into StatefulWidget
:
class CartValueBar extends StatefulWidget {
final double cartValue;
const CartValueBar({Key key, this.cartValue}) : super(key: key);
@override
_CartValueBarState createState() => _CartValueBarState();
}class _CartValueBarState extends State<CartValueBar> {
@override
Widget build(BuildContext context) {
//TODO we'll insert CartValueBar code here
return Container();
}
}
We need to make widget stateful, because in later steps of implementing animation we’ll need something called SingleTickerProviderStateMixin
. This mixin prevents Flutter animations from consuming resources when it’s no longer needed (e.g. it’s not visible anymore).
You can read more about implementing animations in general in Flutter Animations tutorial.
Now, when our widget is stateful, we can add SingleTickerProviderStateMixin
, Animation
and AnimationController
to CartValueBar:
class _CartValueBarState extends State<CartValueBar>
with SingleTickerProviderStateMixin {
AnimationController _controller;
Animation<double> _animation;
@override
void initState() {
super.initState();
_controller = AnimationController(
vsync: this,
duration: Duration(milliseconds: 300),
)..addListener(() {
setState(() {});
});
_animation = Tween<double>(
begin: CART_BAR_HEIGHT,
end: 0,
).animate(
CurvedAnimation(
parent: _controller,
curve: Curves.easeOut,
reverseCurve: Curves.easeIn,
),
);
}
...
@override
void dispose() {
super.dispose();
_controller.dispose();
}
}
Check full source code for this file on GitHub here.
In the beginning, we firstly have AnimationController
, which is responsible in general for running and controlling animation — starting, stopping, pausing, and so on.
The next is Animation
object, which declares more specific information about animation:
- what are animation values? In our case starting from bar height and ending on zero, as it’ll be our bar offset. So the bar will be firstly under the screen when an offset is equal to bar height and at the bottom of the screen when an offset is zero
Note: screen position starts in the left top corner and increases as going to the right bottom corner. So if we’d like to hide the bottom bar, we need to decrease position value.
- what’s the animation interpolator? In our case, we’ll use
CurvedAnimation
with Curves.easeOut and Curves.easeIn interpolators, so the animation will start quickly and ends slowly when showing bar and the opposite when hiding
To run animation, we’ll simply call forward()
on the controller. And at the end, we should call dispose()
on AnimationController when it’s no longer needed. If we want to run animation in the opposite direction, we’ll use reverse()
method.
Now when we have our animation initialized, we need to set animation values to the bar offset, so when the animation will go from bar height to 0, our bar will translate by these values. We’ll use Transform
Widget to do it:
class _CartValueBarState extends State<CartValueBar>
with SingleTickerProviderStateMixin {
...
@override
Widget build(BuildContext context) {
return widget.cartValue != 0
? Align(
alignment: Alignment.bottomCenter,
child: Transform.translate( //changed
offset: Offset(0, _animation.value), //changed
child: Container(
color: Colors.orange,
child: Row(
children: <Widget>[
Padding(
padding: const EdgeInsets.all(16.0),
child: Text(
'Cart value ${_getCartValue()} zł',
),
),
],
),
),
),
)
: Container();
}
...
}
Check full source code for this file on GitHub here.
Ok, so now we have our animation set, values are assigned to translation offset, but what’s missing? Actual animation running.
Where should we start animation?
To decide in what place in the code should we put animation start point, we need to know one important thing about Redux — whenever store emits some change event, our Widgets are rebuilt. So actually with every update, we only know about the current values (in our case, current cart value). It’s not enough to make an animation, because to make it properly, we need to know about the previous state also:
- we want to show cart bar when previous cart value was zero
- we want to hide cart bar when current cart value is zero
We can’t put animation start inside initState()
method, as it’s called only when widgets state is created. After cartValue
changes, our widget is updated, so we need to look for update callback. Luckily for us, we have it! The method we’re looking for is called: didUpdateWidget()
. We can read in the documentation:
If the parent widget rebuilds and request that this location in the tree update to display a new widget with the same runtimeType and Widget.key, the framework will update the widget property of this State object to refer to the new widget and then call this method with the previous widget as an argument.
Override this method to respond when the widget changes (e.g., to start implicit animations).
Great! We can use it for our purpose. In this method we’ll get oldWidget
state, so we can react according to the previous values. Let’s take a look at the code then:
@override
void didUpdateWidget(CartValueBar oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.cartValue == 0) {
_controller.forward();
}
if (widget.cartValue == 0) {
_controller.reverse();
}
}
Check full source code for this file on GitHub here.
Here we’re checking for two conditions. The first situation is when our cart value was previously 0, that means we have to show our cart bar (so call forward()
on animation controller).
The second situation is when new cart value is 0, so we need to hide the cart bar (call reverse()
to play animation in the opposite direction).
Let’s check how it works!
Great, we have animation now! 🎉 But you may have noticed that there’s a little glitch when hiding car bar. That happens because text changes to ‘0.00’ right before going off the screen. We can fix this with simple if statement:
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: Text(
widget.cartValue != 0
? 'Cart value ${_getCartValue()} zł'
: '',
),
),
Now run our animation once again:
Hurray! That’s the effect we wanted! 🚀 We have cool animation in our app now 😎
Cool, it’s the end! Hmm.. Is it?
Actually, we could stop right here and end the article. We achieved what we wanted. But hey, we’re Flutter developers, we know that everything is a widget in Flutter, so… maybe there’s already a Widget that can make this whole “didUpdateWidget()” job for us? Of course, it is.
Let me introduce you AnimatedContainer
:
A container that gradually changes its values over a period of time. The AnimatedContainer will automatically animate between the old and new values of properties when they change using the provided curve and duration.
AnimatedContainer
is, as the name suggests, a Container widget that can be animated. If you’d like to read more about it (or see cool animations that can be done with it!) you can check this great article from Pooja Bhaumik: Flutter Animated Series : Animated Containers. The fact that’s the most interesting for us is that AnimatedContainer uses didUpdateWidget
method under the hood, so it does all the job for us. Let’s try it!
We’ll basically remove all the code responsible for controlling the animations and place AnimatedContainer
instead:
import 'package:flutter/material.dart';const CART_BAR_HEIGHT = 48.0;class CartValueBar extends StatelessWidget {
final double cartValue;
const CartValueBar({Key key, this.cartValue})
: super(key: key);
@override
Widget build(BuildContext context) {
return AnimatedContainer( //changed
height: CART_BAR_HEIGHT,
duration: Duration(milliseconds: 300), //changed
transform: Matrix4.translationValues( //changed
0, cartValue != 0 ? 0 : CART_BAR_HEIGHT, 0), //changed
color: Colors.orange,
child: Row(
children: <Widget>[
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 16.0),
child: Text(
cartValue != 0
? 'Cart value ${_getCartValue()} zł'
: '',
),
),
],
),
);
}
String _getCartValue() => cartValue.toStringAsFixed(2);
}
Check full source code for this file on GitHub here.
As you can see, we needed to add two parameters to our newly created AnimatedContainer — duration for the animation and transform value with translation. If cart value is different than zero, translation value is zero (container is at the bottom of the screen), if cart value is bigger than zero, translation value is equal to bar height (container is under the screen). Whenever this widget is updated (by emitting new store event by Redux), AnimatedContainer
changes transform values with nice animation!
That’s the end!
Ok, that’s the real end of this article.
To sum up, we know now how to create simple animations that can be useful when you have Redux architecture. We also know more about AnimatedContainer. Now it’s your turn to transform this knowledge into super-fancy animations with Flutter!
If you’d like to check code for the full application, you can find it here:
That’s all! Thanks for reading my article 🙌