Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Flutter: Timer Issue During Testing

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?

like image 935
Josh Avatar asked Apr 21 '18 06:04

Josh


People also ask

How do you test a flutter timer?

There are two ways to test timer precision in flutter widgets. First, you can use an action button, like FloatingActionButton, which will update if the counter changes. Second, you can manually run the widget tests.

How do I stop my timer from fluttering?

you can use cancel method to stop the execution of timer by it self based on the condition. Timer. periodic(Duration(seconds: 1), (timer) { if(DateTime. now().

How do you pause and resume timer in flutter?

All you have to do is use timer. reset(); timer. start(); in the timer callback to start it again (make it periodic).


1 Answers

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) {
    ...
  }
}
like image 109
Jonah Williams Avatar answered Sep 17 '22 14:09

Jonah Williams