Why Flutter animations need a vsync/TickerProvider
An article about Ticker and TickerProvider
If you have used Flutter animations before, you likely came across a parameter
named vsync or the mixin SingleTickerProviderStateMixin.
But we are rarely using them ourselves. So what do they even do?
To answer these questions, we will use a simple animation as an example: A clock

For a clock, AnimationController doesn’t seem very useful:
- This is a perpetual animation.
The fact that a controller requires adurationproperty for the controller feels like it doesn’t fit our use-case. - The animation is linear.
We don’t need things like Curve and Tween.
All in all, we may be tempted to not use AnimationController.
A naive approach could be to setState every few frames and use DateTime.now()
like so:
class AnimatedClockClock extends StatefulWidget {@override_AnimatedClockClockState createState() => _AnimatedClockClockState();}class _AnimatedClockClockState extends State<AnimatedClockClock> {var running = true;@overridevoid initState() {super.initState();// Regularly refreshed the screen to update the clockFuture.doWhile(() async {setState(() {});await Future.delayed(const Duration(milliseconds: 50));return running;});}@overridevoid dispose() {// Stop calling setState when the clock is removed from the widget treerunning = false;super.dispose();}@overrideWidget build(BuildContext context) {// A widget that renders an unanimated clock from the current timereturn _ClockRenderer(dateTime: DateTime.now(),);}}
At first glance, it seems to work (the previous gif was recorded using it after-all). But plot twist, it suffers from major drawbacks:
- It’s not very precise.
We need to manually control what the refresh-rate of the animation is. Too low and our screen may not show the updated “seconds” for a few frames. Too high and we are draining the power of our phone. - It’s hard to test.
Because of
Future.delayed, it becomes difficult to target for specific parts of the animation.
But more importantly, it is inefficient:
An animated widget may not be visible
You see, while our Clock widget is inside the widget tree, there is absolutely
no guarantee that it is visible:
- The
Clockmay be in a route that is currently not visible - It may be hidden because an ancestor widget such as Visibility
The problem is, in those situations, our Clock widget will still continuously
call setState, which triggers unnecessary screen refreshes.
One way to highlight this problem is to enable the in-app performance overlay:

From this gif, we can see that even when a new page appeared on the top of our clock, our screen still keeps refreshing. This is bad.
This is why we have that vsync parameter on AnimationController,
Vsync/TickerProvider allows animations to be muted, slowed, or fast-forwarded
Be it tests, dev tools, or because the animation is no longer visible, the speed of our animation may depend on factors external to the animation itself.
Using Flutter’s animation framework, this is achieved through a Ticker + TickerProvider.
- Ticker is the refresh rate of our animations. This is what we want to pause when our clock is hidden.
- TickerProvider, usually implemented with SingleTickerProviderStateMixin,
is a binding between Ticker and external factors.
This is what will pause our Tickers.
More specifically, when writing:
class Example extends StatefulWidget {@overrideExampleState createState() => ExampleState();}class ExampleState extends State<Example> with SingleTickerProviderStateMixin {AnimationController controller;@overridevoid initState() {super.initState();controller = AnimationController(duration: Duration(),vsync: this,);}// ...}
Then we are saying “the animation associated to controller can be paused
when our Example widget is hidden”.
Reimplementing our Clock with Ticker/TickerProvider
Let’s fix the performance issues of our clock by using Ticker/TickerProvider to control the animation.
Creating a Ticker with TickerProvider
Our first goal is to create a Ticker. For this, we will need a TickerProvider, such as SingleTickerProviderStateMixin.
It is important to use an official TickerProvider, as we could otherwise break the “pause the animation” feature.
Don’t create one TickerProvider and reuse it for all widgets either.
Your TickerProvider should be associated with the widget that wants to perform
an animation. As such, avoid storing a TickerProvider as a global/singleton
or passing it with provider.
A good rule of thumb is to accept that StatefulWidget + SingleTickerProviderStateMixin
is verbose, and not fight against it.
Anyway, let’s create our Ticker:
class AnimatedClock extends StatefulWidget {@override_AnimatedClockState createState() => _AnimatedClockState();}class _AnimatedClockState extends State<AnimatedClock>with SingleTickerProviderStateMixin {Ticker _ticker;@overridevoid initState() {super.initState();_ticker = createTicker((Duration elapsed) {});}@overridevoid dispose() {_ticker.dispose();super.dispose();}}
Do not forget to call dispose on your Ticker when the state is destroyed.
Using our Ticker to keep track of the current time
Now that we have created a Ticker, let’s start using it. We will want to do two things:
- Know what the time was when the animation started
- Update that time on every tick based on how much time has elapsed since the beginning of the animation.
First, let’s obtain the current time when the animation starts. Nothing special
here, we will simply call DateTime.now() (although we will come back to this
code later for testing).
Ticker _ticker;DateTime _startTime;DateTime _currentTime;@overridevoid initState() {super.initState();_startTime = _currentTime = DateTime.now();_ticker = createTicker((Duration elapsed) {});}
Then, we will use that _currentTime variable inside our build method to render our clock:
@overrideWidget build(BuildContext context) {return _ClockRenderer(dateTime: _currentTime);}
Finally, we will use our Ticker to update our _currentTime variable.
This can be done with the elapsed parameter of the function passed when creating
our Ticker.
That elapsed parameter represents how much time has passed since the beginning
of the animation. As such, the current time is the time when the animation started +
the time elapsed.
We can update our createTicker with this logic:
_ticker = createTicker((Duration elapsed) {setState(() {_currentTime = _startTime.add(elapsed);});});
That’s it! At this stage, we should have a working clock.
The difference with before? Let’s do the same test (pushing a new route) with the
performance overlay enabled:

As you can see from this new gif, now as soon as a new route is pushed, the screen stops refreshing: Our clock animation is paused.
Writing tests for our clock
It would be a shame to stop here.
Now that we’ve used Ticker for our animation, let’s showcase how Ticker
also makes your animations testable.
For this, we will use “golden tests”, which consists of taking screenshots of our widget at specific times.
A naive approach would be:
testWidgets('renders our clock', (tester) async {await tester.pumpWidget(MaterialApp(home: Center(child: RepaintBoundary(child: AnimatedClock(),),),),);await expectLater(find.byType(AnimatedClock),matchesGoldenFile('initial_frame.png'),);});
But this causes two questions:
-
Our widget relies on
DateTime.now(), so the screenshot will never be the same. How can we make sure that the widget is always rendered at a specific time? -
How do we make the animation progress?
We can’t reasonably useawait Future.delayedand make our test wait for an hour.
Mocking the time
First, we will want to mock the behavior of DateTime.now inside our tests.
For this, we can use a class called Clock, from package:clock. It’s a package made by Google developers to help with time manipulation with testability in mind.
This can be combined with provider to provide an instance of Clock to our application, such that instead of:
DateTime _startTime;DateTime _currentTime;@overridevoid initState() {super.initState();_startTime = _currentTime = DateTime.now();
we do:
DateTime _initialTime;DateTime _now;@overridevoid initState() {super.initState();_now = _initialTime = context.read<Clock>().now();
From there, we need to add a provider in our main that creates a Clock:
void main() {runApp(Provider(create: (_) => Clock(),child: MyApp(),),);}
Then, our tests can override this provider with a mocked Clock that returns
a specific time.
Widget tests come with a built-in mocked clock, so we will use that:
testWidgets('renders our clock', (tester) async {await tester.pumpWidget(Provider(create: (_) => tester.binding.clock,child: ...,),);...}
That’s it for mocking time!
Our test at this stage should look like this:
testWidgets('renders our clock', (tester) async {await tester.pumpWidget(Provider(create: (_) => tester.binding.clock,child: MaterialApp(home: Center(// Adding a RepaintBoundary ensure that we take a sreenshot of only// what we care about, not the entire screen.child: RepaintBoundary(child: AnimatedClock(),),),),),);await expectLater(find.byType(AnimatedClock),matchesGoldenFile('initial_frame.png'),);});
Which generates the following initial_frame.png:

Simulating time changes
Now that we have mocked the initial frame of our test, what remains is testing the scenario where our clock updates.
What we don’t want is to use Future.delayed, as this is very inefficient.
Instead, thanks to Ticker, we will ask our test to simulate the passage of time.
This is done using tester.pump, by passing to pump how much time we want to skip.
We can then take another screenshot.
This will typically look like this:
// added at the end of our previous testawait tester.pump(Duration(hours: 2, minutes: 42, seconds: 21));await expectLater(find.byType(AnimatedClock),matchesGoldenFile('updated_frame.png'),);
Which generates the following screenshot:

Done!
Thanks to Ticker, we now have both fixed performance issues with our clock and we were able to write UI tests for our animation.
For the full code of both the test and the widgets, you can see the following gist:
https://gist.github.com/rrousselGit/beaf7442a20ea7e2ed3f13bbd40984a8
Twitter
Google+
Facebook
Reddit
LinkedIn
Email