AnimationController
is the big daddy of animations in Flutter. You can use it to forward, reverse, or repeat animations, and much more.
But setting up an AnimationController
requires a lot of ceremony:
// 1. declare a StatefulWidget
class RotatingContainer extends StatefulWidget {
@override
_RotatingContainerState createState() => _RotatingContainerState();
}
class _RotatingContainerState extends State<RotatingContainer>
// 2. add SingleTickerProviderStateMixin
with SingleTickerProviderStateMixin {
// 3. declare the animation controller
late final AnimationController _animationController;
@override
void initState() {
super.initState();
// 4. initialize the animation controller
_animationController = AnimationController(
vsync: this,
duration: Duration(milliseconds: 750),
);
}
@override
void dispose() {
// 5. dispose when done
_animationController.dispose();
super.dispose();
}
// build method, etc.
}
This is all before we even write any animation code. If we want to use this to show a rotating box, we can call _animationController.repeat();
inside initState()
and add this code in the build()
method:
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: _animationController,
child: Container(color: Colors.red),
builder: (context, child) {
return Transform(
alignment: Alignment.center,
transform: Matrix4.rotationZ(2 * pi * _animationController.value),
child: child,
);
},
);
}
Note: the red
Container
is passed as a child to theAnimatedBuilder
and reused inside the builder. See "Why do TweenAnimationBuilder and AnimatedBuilder have a child argument?" for an explanation of this technique.
If you have a lot of explicit animations, it is difficult to reuse the code in initState()
and dispose()
, and copy-pasting all the boilerplate across widgets is error-prone.
So let's explore two ways of reducing the boilerplate code.
1. Use Flutter Hooks
The flutter_hooks package makes working with AnimationController
dead simple:
// Note: we are extending `HookWidget`
class RotatingContainer extends HookWidget {
@override
Widget build(BuildContext context) {
// All the steps above are replaced by a single line:
final controller = useAnimationController(duration: Duration(seconds: 2))
..repeat(); // start the animation
return AnimatedBuilder(
animation: controller,
child: Container(color: Colors.red),
builder: (context, child) {
return Transform(
alignment: Alignment.center,
transform: Matrix4.rotationZ(2 * pi * controller.value),
child: child,
);
},
);
}
}
All the magic for obtaining the TickerProvider
, initializing and disposing the AnimationController
happens inside the useAnimationController()
method.
Make sure to read the documentation for flutter_hooks to understand how to use this correctly.
Flutter Hooks are a great solution for this use case. If you are already using them in your project, this is a no-brainer. 👍
But if you don't want to have flutter_hooks as an extra dependency (for example because you're writing a package), there are other ways to reduce the boilerplate code.
2. Extending the State class
You can create a AnimationControllerState
subclass of State<T>
to contain all the AnimationController
logic:
import 'package:flutter/material.dart';
abstract class AnimationControllerState<T extends StatefulWidget>
extends State<T> with SingleTickerProviderStateMixin {
AnimationControllerState(this.animationDuration);
final Duration animationDuration;
late final animationController = AnimationController(
vsync: this,
duration: animationDuration
);
@override
void dispose() {
animationController.dispose();
super.dispose();
}
}
Here's the original example, updated to use this:
class RotatingContainer extends StatefulWidget {
@override
_RotatingContainerState createState() =>
_RotatingContainerState(Duration(seconds: 2));
}
class _RotatingContainerState
extends AnimationControllerState<RotatingContainer> {
// add a constructor so we can pass the duration to the parent class
_RotatingContainerState(Duration duration) : super(duration);
// TODO: initState & build methods
}
In this case we still need a StatefulWidget
. But all the initialization and dispose logic is in AnimationControllerState
, which is now the parent of _RotatingContainerState
.
We can complete the example like this:
@override
void initState() {
super.initState();
// animationController is defined in the parent class, so we can use it here
animationController.repeat();
}
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: animationController,
child: Container(color: Colors.red),
builder: (context, child) {
return Transform(
alignment: Alignment.center,
transform: Matrix4.rotationZ(2 * pi * animationController.value),
child: child,
);
},
);
}
This is a bit more code than the flutter_hooks approach, but we don't need the additional package dependency.
If you don't mind having to create a StatefulWidget
for each explicit animation in your app, this is a good solution. Many use cases that need explicit animations require some state variables anyway, so I find this acceptable in practice.
useAnimationController()
is just one of many hooks that you can use. You can use flutter_hooks to manage state and much more. Make sure to learn about the various kinds of hooks to make the most of this package.
Almost forgot, here's the running app from the example above:
Wrap Up
We have explored two ways to reduce AnimationController
boilerplate:
- use
useAnimationController()
from the flutter_hooks package - implement
AnimationControllerState
and reuse it as needed
Now it's your turn
Are you ready to delete a lot of unnecessary code from your Flutter apps?
- do a project-wide search for
AnimationController
- replace all the boilerplate code with your favourite method (
useAnimationController()
orAnimationControllerState
) - test everything, make a PR and see how much code you saved
You're welcome! 😀