jld3103
2 years ago
7 changed files with 171 additions and 4 deletions
@ -0,0 +1,85 @@ |
|||||||
|
part of '../../neon.dart'; |
||||||
|
|
||||||
|
abstract class TimerBlocEvents { |
||||||
|
/// Register a [callback] that will be called periodically. |
||||||
|
/// The time between the executions is defined by the [duration]. |
||||||
|
NeonTimer registerTimer(final Duration duration, final VoidCallback callback); |
||||||
|
|
||||||
|
/// Unregister a timer that has been previously registered with the bloc. |
||||||
|
/// You can also use [NeonTimer.cancel]. |
||||||
|
void unregisterTimer(final NeonTimer timer); |
||||||
|
} |
||||||
|
|
||||||
|
abstract class TimerBlocStates {} |
||||||
|
|
||||||
|
/// Execute callbacks at defined periodic intervals. |
||||||
|
/// Components can register their callbacks and everything with the same periodicity will be executed at the same time. |
||||||
|
/// |
||||||
|
/// The [TimerBloc] is a singleton. |
||||||
|
/// Sub-second timers are not supported. |
||||||
|
class TimerBloc extends Bloc implements TimerBlocEvents, TimerBlocStates { |
||||||
|
factory TimerBloc() => instance ??= TimerBloc._(); |
||||||
|
|
||||||
|
@visibleForTesting |
||||||
|
factory TimerBloc.mocked(final TimerBloc mock) => instance ??= mock; |
||||||
|
|
||||||
|
TimerBloc._(); |
||||||
|
|
||||||
|
@visibleForTesting |
||||||
|
static TimerBloc? instance; |
||||||
|
|
||||||
|
final Map<int, Timer> _timers = {}; |
||||||
|
final Map<int, Set<VoidCallback>> _callbacks = {}; |
||||||
|
|
||||||
|
@visibleForTesting |
||||||
|
Map<int, Timer> get timers => _timers; |
||||||
|
|
||||||
|
@visibleForTesting |
||||||
|
Map<int, Set<VoidCallback>> get callbacks => _callbacks; |
||||||
|
|
||||||
|
@override |
||||||
|
void dispose() { |
||||||
|
for (final timer in _timers.values) { |
||||||
|
timer.cancel(); |
||||||
|
} |
||||||
|
_timers.clear(); |
||||||
|
_callbacks.clear(); |
||||||
|
TimerBloc.instance = null; |
||||||
|
} |
||||||
|
|
||||||
|
@override |
||||||
|
NeonTimer registerTimer(final Duration duration, final VoidCallback callback) { |
||||||
|
if (_timers[duration.inSeconds] == null) { |
||||||
|
_timers[duration.inSeconds] = Timer.periodic(duration, (final _) { |
||||||
|
for (final callback in _callbacks[duration.inSeconds]!) { |
||||||
|
callback(); |
||||||
|
} |
||||||
|
}); |
||||||
|
_callbacks[duration.inSeconds] = {callback}; |
||||||
|
} else { |
||||||
|
_callbacks[duration.inSeconds]!.add(callback); |
||||||
|
} |
||||||
|
return NeonTimer(duration, callback); |
||||||
|
} |
||||||
|
|
||||||
|
@override |
||||||
|
void unregisterTimer(final NeonTimer timer) { |
||||||
|
if (_timers[timer.duration.inSeconds] != null) { |
||||||
|
_callbacks[timer.duration.inSeconds]!.remove(timer.callback); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
class NeonTimer { |
||||||
|
NeonTimer( |
||||||
|
this.duration, |
||||||
|
this.callback, |
||||||
|
); |
||||||
|
|
||||||
|
final Duration duration; |
||||||
|
final VoidCallback callback; |
||||||
|
|
||||||
|
void cancel() { |
||||||
|
TimerBloc().unregisterTimer(this); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,43 @@ |
|||||||
|
import 'package:neon/neon.dart'; |
||||||
|
import 'package:test/test.dart'; |
||||||
|
|
||||||
|
void main() { |
||||||
|
group('TimerBloc', () { |
||||||
|
tearDown(() { |
||||||
|
TimerBloc().dispose(); |
||||||
|
}); |
||||||
|
|
||||||
|
test('Register timer', () async { |
||||||
|
const duration = Duration(milliseconds: 100); |
||||||
|
|
||||||
|
final stopwatch = Stopwatch()..start(); |
||||||
|
final callback = stopwatch.stop; |
||||||
|
TimerBloc().registerTimer(duration, callback); |
||||||
|
await Future.delayed(duration); |
||||||
|
|
||||||
|
expect(stopwatch.elapsedMilliseconds, greaterThan(duration.inMilliseconds)); |
||||||
|
expect(stopwatch.elapsedMilliseconds, lessThan(duration.inMilliseconds * 1.1)); |
||||||
|
expect(TimerBloc().callbacks[duration.inSeconds], contains(callback)); |
||||||
|
expect(TimerBloc().timers[duration.inSeconds], isNot(isNull)); |
||||||
|
}); |
||||||
|
|
||||||
|
test('Unregister timer', () async { |
||||||
|
const duration = Duration(milliseconds: 100); |
||||||
|
final callback = neverCalled; |
||||||
|
|
||||||
|
TimerBloc().registerTimer(duration, callback).cancel(); |
||||||
|
await Future.delayed(duration); |
||||||
|
|
||||||
|
expect(TimerBloc().callbacks[duration.inSeconds], isNot(contains(callback))); |
||||||
|
}); |
||||||
|
|
||||||
|
test('dispose', () { |
||||||
|
TimerBloc().registerTimer(const Duration(minutes: 1), () {}); |
||||||
|
expect(TimerBloc().timers, hasLength(1)); |
||||||
|
expect(TimerBloc().callbacks, hasLength(1)); |
||||||
|
TimerBloc().dispose(); |
||||||
|
expect(TimerBloc().timers, isEmpty); |
||||||
|
expect(TimerBloc().callbacks, isEmpty); |
||||||
|
}); |
||||||
|
}); |
||||||
|
} |
Loading…
Reference in new issue