Search code examples
flutterdartflutter-provider

Timer.periodic does not work correctly with provider


I am trying to use the method periodic of the class Timer inside a class which extends of the class ChangeNotifier (of the package Provider) making my variable time decrease every second.

This works correctly if I don't add the NotifyListeners method which redraws all the widgets that occupy the time property, like:

class PriceProvider extends ChangeNotifier{
  int _time = 60;

  int get time{
    return _time;
  }

  void chronometer(){//method which activate the timer

    Timer _timer = Timer.periodic(const Duration(seconds: 1), (Timer timer){
      print(DateTime.now());//I print the date so you can see how often the code is executed
      
      _time += -1;//decrease time

      if(_time == 0){
        _time = 60;
      } 

      // notifyListeners(); 
    });
  } 

}

Console output (runs every second correctly): Salida por consola

On the other hand, if I uncomment the NotifyListeners method, the code starts executing more and more times per second exponentially (for example, first it executes once, then twice, then 5, then 9 and so on): Salida por consola

Here is the code where I call the method chronometer:

class PriceWithClock extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    PriceProvider priceProvider = Provider.of<PriceProvider>(context);
    priceProvider.chronometer();
    return CircularPercentIndicator(
      radius: 100.0,
      lineWidth: 5.0,
      percent: 1-priceProvider.time/60,
      center: Text("00:${priceProvider.time}"),
     ),
    );
  }
}

Solution

  • You should not put the priceProvider.chronometer(); inside the build method since it will be executed on every PriceWithClock widget build. And in your case, it would happen every second.

    What you can do, is create a Stateful widget and trigger the chronometer inside the initState() method:

    class PriceWithClock extends StatefulWidget {
      @override
      State<StatefulWidget> createState() => _PriceWithClockState();
    }
    
    class _PriceWithClockState extends State<PriceWithClock> {
      @override
      void initState() {
        super.initState();
    
        context.read<PriceProvider>().chronometer();
      }
    
      @override
      Widget build(BuildContext context) {
        ...
      }
    }
    

    As for the build method, you could use the context.watch Provider extension method to listen to the updated time value:

    class _PriceWithClockState extends State<PriceWithClock> {
      ...
    
      @override
      Widget build(BuildContext context) {
        final time = context.watch<PriceProvider>().time;
        
        return CircularPercentIndicator(
          radius: 100.0,
          lineWidth: 5.0,
          percent: 1 - time / 60,
          center: Text("00:$time"),
        );
      }
    }
    

    The final result looks like this:

    class PriceWithClock extends StatefulWidget {
      @override
      State<StatefulWidget> createState() => _PriceWithClockState();
    }
    
    class _PriceWithClockState extends State<PriceWithClock> {
      @override
      void initState() {
        super.initState();
    
        context.read<PriceProvider>().chronometer();
      }
    
      @override
      Widget build(BuildContext context) {
        final time = context.watch<PriceProvider>().time;
        
        return CircularPercentIndicator(
          radius: 100.0,
          lineWidth: 5.0,
          percent: 1 - time / 60,
          center: Text("00:$time"),
        );
      }
    }