I have a periodic timer in one of my StatelessWidget
's. Without going too much into detail, here is a snippet of code producing a timer:
class AnniversaryHomePage extends StatelessWidget {
. . .
void _updateDisplayTime(StoreInheritedWidget inheritedWidget) {
String anniversaryString = inheritedWidget.prefs.getString('anniversaryDate');
inheritedWidget.store.dispatch(DateTime.parse(anniversaryString));
}
/// Widget Methods
@override
Widget build(BuildContext context) {
final inheritedWidget = StoreInheritedWidget.of(context);
new Timer.periodic(this.refreshRate, (Timer timer) => _updateDisplayTime(inheritedWidget));
. . .
}
When I try pumping my the application's starting point into flutter test
, I get the following error message:
══╡ EXCEPTION CAUGHT BY FLUTTER TEST FRAMEWORK ╞════════════════════════════════════════════════════
The following assertion was thrown running a test:
A periodic Timer is still running even after the widget tree was disposed.
'package:flutter_test/src/binding.dart': Failed assertion: line 668 pos 7:
'_fakeAsync.periodicTimerCount == 0'
The question is, am I using my Timer.periodic
incorrectly? If not, how do I mitigate this error?
The issue is that creating a Timer creates a resource which must be disposed, and therefore your widget is actually Stateful and not stateless. Specifically, the build
method may be called 60 times a second (or more if the platform is 120fps). Any less is just an optimization.
You have a very critical bug in your code - the build
method is creating a new Timer every time it is called. And since your timers are never cancelled, you could have hundreds or potentially thousands of events dispatched to your store.
To avoid situations like this, the framework has a State
class, with an initState
and dispose
lifecycle. The framework promises that if it rebuilds your widget, it won't call initState
more than once and it will always call dispose
. This allows you to create a Timer once and reuse it on subsequent calls to build
.
For example, you can move most of your logic to the State like below. Leave the refreshRate on the widget, and you can even cancel and update your timer using the didUpdateWidget
lifecycle too.
class AnniversaryHomePage extends StatefulWidget {
@override
State createState() => new AnniversaryHomePageState();
}
class AnniversaryHomePageState extends State<AnniversaryHomePage> {
Timer _timer;
@override
void initState() {
_timer = new Timer.periodic(widget.refreshRate,
(Timer timer) => _updateDisplayTime(inheritedWidget));
super.initState();
}
@override
void dispose() {
_timer.cancel();
super.dispose();
}
@override
Widget build(BuildContext context) {
...
}
}