Geek Culture
Published in

Geek Culture

Timer Precision in Flutter

A Flutter Case Study

Intro

I came across a post on StackOverflow regarding Flutter’s (Dart’s) Timer class. The user noted the variation with which the Timer's callback was firing, feeling that it was not working as intended. Specifically, the user stated that while setting the Timer's period to 20 milliseconds, the time between execution frequently fell within the unacceptable range of 100–1000 ms. To understand what’s going on here and how to solve it, we should start by taking a look at Dart’s Timer class.

Timer

The Timer class in Dart is a powerful means to schedule async tasks that handle work in the background once or repeatedly. However, if you’ve worked with it, you may have noticed that the actual timing is not very precise (Not very intuitive for a class called Timer 🤔). This is particularly noticeable when using Timer.periodic. If we look into the code for this class, we will find the following comment:

The exact timing depends on the underlying timer implementation. No more than `n` callbacks will be made in `duration * n` time, but the time between two consecutive callbacks can be shorter and longer than `duration`.

It seems that the nature of Timer alongside Dart’s Event Loop is responsible in the variation of callback durations. Certainly, as more events are added to the loop, we can see how it could effect the timing. If we need to update a Widget in regular intervals (i.e. every frame), then Timer is obviously not the best approach. If you are not familiar with Dart’s Event Loop, check out the video below.

Ticker

Fortunately, there is a timing mechanism in Flutter that we can count on to be very consistent. It is tied to the drawing of frames at a consistent 60 fps, and since it fires before each frame is drawn, it is the perfect way to update our UI as frequently and consistently as possible! This approach uses the Ticker class from Flutter’s scheduler library. Let’s check it out.

We will be creating a simple stopwatch widget. First, let’s define our Stopwatch widget and State. We’ll use SingleTickerProviderStateMixin to provide access to a Ticker in our State. If you’ve worked animations in Flutter, you’ve probably already come across it. We’ll also provide variables for tracking the elapsed time.

class Stopwatch extends StatefulWidget {
const Stopwatch({Key? key}) : super(key: key);
@override
State<Stopwatch> createState() => _StopwatchState();
}
class _StopwatchState extends State<Stopwatch>
with SingleTickerProviderStateMixin {
var elapsed = Duration.zero;
var previousElapsed = Duration.zero;
@override
Widget build(BuildContext context) {
// TODO: implement UI
}
}

We are defining previousElapsed to help us keep track of the duration when pausing & restarting the stopwatch. Next, we will create our Ticker using the aptly named createTicker method provided by the SingleTickerProviderStateMixin. I like to use the late keyword to instantiate the variable, but you may prefer to define it first and instantiate it in initState. Either way, be sure to dispose of the Ticker.

late final ticker = createTicker((elapsed) {
setState(() => this.elapsed = elapsed);
});
@override
void dispose() {
ticker.dispose();
super.dispose();
}

As you can see, we are simply defining a callback that updates our elapsed Duration and calls setState. Now, let’s define our start, stop, and reset methods.

void start() {
previousElapsed += elapsed;
ticker.start();
}
void stop() => setState(() => ticker.stop());void reset() {
stop();
setState(() => elapsed = previousElapsed = Duration.zero);
}

These methods are pretty straight forward, but note where we are updating previousElapsed do ensure we don’t lose track when starting & stopping without a reset. Also note that we call setState from stop & reset to update the UI. It is not necessary when starting the Ticker as our callback is calling setState each frame.

The only thing left to do is implement our build method to use our Duration variables to show the elapsed time.

@override
Widget build(BuildContext context) {
final totalElapsed = elapsed + previousElapsed;
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
FormattedDuration(elapsed: totalElapsed),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
IconButton(
onPressed: reset,
icon: const Icon(Icons.restart_alt),
),
IconButton(
onPressed: ticker.isTicking ? stop : start,
icon: Icon(ticker.isTicking ? Icons.stop : Icons.play_arrow),
),
],
),
],
);
}

For the sake of brevity, I have left out any extraneous formatting and layout code. The important parts to note are that we calculate the totalElapsed Duration by adding elapsed & previousElapsed and the use of ticker.isTicking. This property is true if the Ticker has scheduled a call to its callback on the next frame, meaning it is running.

NOTE: Ticker also has an isActive property. isActive can be true even when the Ticker is muted (not actively scheduling calls to its callback). That is why we use isTicking here.

Caveats

This approach is perfect for any scheduled task that will update your apps UI, as it is guaranteed to be called once per frame. You can also use it to schedule any other background task that needs to occur no more than 60 times per second (approx. every 16.6 milliseconds). For instance, you could play a metronome sound at 120 bpm (twice per second/once every 30 frames).

However, if you need to repeat tasks more frequently, then this is not appropriate. This approach is also Flutter specific, so it does not apply if you are working in Dart alone. If either of those cases applies to you, I suggest you check out this library. It creates an Isolate for precision timing. I am not the author and do not have extensive experience with it, so your mileage may vary. The author states the accuracy to be within a few milliseconds. It should be noted though, that communication between Isolates is asynchronous, and subject to irregularities, so keep that in mind as you plan your project.

Conclusion

I hope you’ve enjoyed this read and learned something valuable along the way. Tickers are a powerful part of Flutter’s scheduling library that can help us guarantee timing alongside every frame.

As always, thanks for reading! Comments are encouraged, and claps are appreciated. Please follow to keep up with my Flutter articles and more.

--

--

--

A new tech publication by Start it up (https://medium.com/swlh).

Recommended from Medium

WAX Technical How To #4

Connecting a Flask Application to a Raspberry Pi PostgreSQL server

Small Steps To Refactoring Legacy Projects

Magento 1 vs Magento 2: A worthful comparison

Behavioral Design Pattern: State

A story of how happy faces can be bad for your software

Duolingo Hacks XP Tricks

Get Hired At Your Dream Job In Just 11 Lines Of Python Code

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store
Lee Phillips

Lee Phillips

Software developer. Flutter fanatic. Other interests include photography, sports, coffee, and food.

More from Medium

Dynamically Pinned List Headers

How to Optimize Flutter Web and How Flutter Web work in Html Renderer

4 lesser known but awesome Flutter resources

How to make Twitter-like post GUI in Flutter