Currently we can't pause a Timer, just cancel it, having a mechanism for pausing, resuming and restarting a Timer would make it a lot more powerful.
A restart or reset functionality requires information that the primitive Timer doesn't even store, like, how long the duration originally was. The internal (non-periodic) timer representation only needs to know when to trigger, it doesn't need to know when it started, so adding a reset functionality to it would require storing more information, which isn't actually used in all existing uses of Timer.
It's also functionality you can easily build on top of the basic Timer, for when you really need it. Perhaps something like:
// Copyright 2020 Google LLC.
// SPDX-License-Identifier: BSD-3-Clause
import "dart:async";
class ComplexTimer implements Timer {
final Zone _zone;
Stopwatch _activeTimer = Stopwatch();
Timer? _timer;
final Duration _originalDuration;
void Function(ComplexTimer)? _onTimeout;
int _tick = 0;
ComplexTimer(Duration duration) : _originalDuration = duration, _zone = Zone.current {
_startTimer();
}
set onTimeout(void Function(ComplexTimer)? callback) {
_onTimeout = callback == null ? null : _zone.bindUnaryCallback(callback);
}
int get tick => _tick;
// Whether the timer is actively counting.
bool get isActive => _timer?.isActive ?? false;
// Whether the timer is started, but not currently actively counting.
bool get isPaused {
var timer = _timer;
return timer != null && !timer.isActive;
}
// Whether the timer has expired.
bool get isExpired => _timer == null;
/// Pauses an active timer.
///
/// Nothing happens if the timer is already paused or expired.
void pause() {
_pauseTimer();
}
/// Resumes counting when paused.
///
/// Nothing happens if the timer is active or expired.
void resume() {
var timer = _timer;
if (timer == null || timer.isActive) return;
_startTimer();
}
/// Resets the timer.
///
/// Sets the timer to its original duration.
/// Does not change whether the timer is active, paused or expired.
void reset() {
var timer = _timer;
if (timer == null) return; // is expired.
_activeTimer..stop()..reset();
if (timer.isActive) {
timer.cancel();
_startTimer();
}
}
/// Restarts the timer.
///
/// Starts counting for the original duration.
/// Works whether the timer is active, paused or expired.
void restart() {
_timer?.cancel();
_activeTimer..stop()..reset();
_startTimer();
}
/// Stops and expires the current timer.
///
/// Nothing happens if the timer is already expired.
void cancel() {
_timer?.cancel();
_expireTimer();
}
void _startTimer() {
var elapsed = _activeTimer.elapsedMilliseconds;
var duration = _originalDuration;
if (elapsed > 0) {
duration = Duration(
milliseconds: _originalDuration.inMilliseconds - elapsed);
}
_timer = _zone.createTimer(duration, _onTick);
_activeTimer.start();
}
void _expireTimer() {
_timer = null;
_activeTimer..stop()..reset();
}
void _pauseTimer() {
_timer?.cancel();
_activeTimer..stop();
}
void _onTick() {
_tick++;
_expireTimer();
var callback = _onTimeout;
if (callback != null) {
_zone.runUnary(callback, this);
}
}
}
@lrhn thanks for the code, I am currently using Stream.periodic() as it provides pause and resume, through the Stream, but I still think a complete timer will be a great addition to the SDK.
Could you explain me why the Timer callback has to be bind to a zone and not called directly? Also, why do we need a zone to create a Timer? I imagine there's a whole architectural reason, probably related to the underlying platform actually running the code, which can impact a lot the precision of timers (javascript vs compiled code). But if its too complicated to just answer here it's fine, don't want to bother you with a mere curiosity :).
Dart generally calls asynchronous callbacks in the zone where they were originally "created".
Such callbacks are first registered, because that makes it possible for special zones to record more information about the point where the callback is created (say, remember the stack trace, which is what the stack_trace package does).
That is, we call Zone.registeCallback to enable zones to know about the callback creation as well as the later Zone.run for running it.
If you just store the callback, but don't register it at the time it's stored, then we can still run it in the correct zone when necessary, but such special zones would stop working for that callback.
hmm interesting, I will try to look for more info on this, sounds like a "trap for young players", but last time I checked the Zones docs they were outdated and incomplete :/
It looks like it's not that trivial to implement the missing functionality, so it might be definitely a good idea to provide a correct implementation of a Timer that can be paused by the SDK.
Since I needed this and was planning to write my own when I bumped into this issue (I looked hard and couldn't find any existing pub package implementing it), I ended up creating a library to provide this simple pausable timer. I based it on this one, without being an expert on dart at all (I started using it 3 months ago and this is my first pub package). Since it is heavily inspired in the implementation in https://github.com/dart-lang/sdk/issues/43329#issuecomment-687024252, I would like to ask you, @lrhn, if it is OK. I put it under BSD 3-clause as I saw is the license used in general by the Dart project. I also make explicit mention about this comment in the code. If you are good with all this, I will publish it as a package in pub.
https://github.com/llucax/pausable_timer
I would appreciate reviews also! Thanks.
As I mention on a previous comment, Stream.periodic has similar functionality.
Yeah, thanks, but the interface is not very intuitive for this use case. I thought of also implementing it using Stream.periodic, but it looked like less boilerplate to do so directly.
@llucax I've put an explicit license on the code, so go ahead and use it under that license. (It is the same 3-clause BSD that we otherwise use).
@lrhn OK, cool. I'll update the LICENSE file then. Thanks!
Published. Let me know if something doesn't look good, I'll be happy to fix it.
Published. Let me know if something doesn't look good, I'll be happy to fix it.
bro the timer package that you made is it not periodic ?
I tested it to run a countdown for 10 seconds but , it accepts only void callback function(as a 2nd parameter) which is called just once and is not periodic.
How to implement countdown if it just fires after the given duration (read the whole docs provided in the package, no where it mentions how to implement this.)
Yeah, there is no periodic support for now, but you can just rearm the timer when it is fired and you get the periodicity, something like:
var timer;
timer = PausableTimer(Duration(seconds: 1), () {
yourCallback();
timer..reset()..start();
});
Should do the trick (you might want to rearm the timer before the callback is called if it takes some time to complete and you want more precision in the period).
But the whole idea of the package is to be convenient to use, so feel free to create a feature request to add a periodic version!
PS: I'm not really sure what you mean with "implement countdown".
Hi @singlesoup, I did a minor release including an example with what I guess is what you wanted: https://pub.dev/packages/pausable_timer#example-pausable-countdown-implementation (or to download). I hope it helps!
This works like charm!!
Let me try what else i can do with it, on Flutter..
Most helpful comment
Since I needed this and was planning to write my own when I bumped into this issue (I looked hard and couldn't find any existing pub package implementing it), I ended up creating a library to provide this simple pausable timer. I based it on this one, without being an expert on dart at all (I started using it 3 months ago and this is my first pub package). Since it is heavily inspired in the implementation in https://github.com/dart-lang/sdk/issues/43329#issuecomment-687024252, I would like to ask you, @lrhn, if it is OK. I put it under BSD 3-clause as I saw is the license used in general by the Dart project. I also make explicit mention about this comment in the code. If you are good with all this, I will publish it as a package in pub.
https://github.com/llucax/pausable_timer
I would appreciate reviews also! Thanks.