Search code examples
androiddartflutterdart-async

How to implement persistent stopwatch in Flutter?


I am implementing a timer in flutter .Here is the structure of the app.

Page A (Contains some lists where user clicks and takes it to timer Page). Page B formats ,runs the timer .I am able to run the timer/stopwatch properly,but when i press the back button on Page B I get the setstate() called after dispose error.I understand that this is the expected behaviour. If i use timer.cancel() on dispose I wont be getting the error ,but the timer will stop running.The timer/stopwatch should continue to run even if i navigate to Page A or say any other new page(widget). I know that this may be possible using listeners and WidgetBindingObserver,But i have no clear knowledge of implementing it.Hope I'll get some help on this issue.

Build class of page B:

  Widget build(BuildContext context) {
return Scaffold(
    appBar: AppBar(
      leading: new IconButton(icon: new Icon(Icons.arrow_back), onPressed: ()async{
        Navigator.pop(context,widget._elapsedTime);
      }),
      title: Text("widget.title"),
    ),
    body: Center(
      child: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: <Widget>[
          Text(
            '$_elapsedTime'),
          RaisedButton(
            child: Text('Start'),
            onPressed: () { 
              if(watch.isRunning){
                stopWatch();
              }
              else{
               startWatch();
              }
            },
          ),

        ],
      ),
    ));

StartWatch function:

startWatch() {
watch.start();
timer = new Timer.periodic(new Duration(milliseconds:1000), updateTime);}

Update Time function which gets called every second:

updateTime(Timer timer) {
   if (watch.isRunning) {
   print(_elapsedTime);
   var time= formatedTime(watch.elapsedMilliseconds);
   print("time is"+time);
   setState(() {
        _elapsedTime = time;
   });
 }

Solution

  • Here is a minimal working solution. Key points:

    • Introduction of a TimerService class that isolates the timer functionality
    • TimerService implements ChangeNotifier, which you can subscribe to to receive changes.
    • An InheritedWidget is used to provide the service to all widgets of your app. This inherited widget wraps your app widget.
    • AnimatedBuilder is used to receive changes from the ChangeNotifier. Subscriptions are handles automatically (no manual addListener/removeListener).

    import 'dart:async';
    
    import 'package:flutter/material.dart';
    
    void main() {
      final timerService = TimerService();
      runApp(
        TimerServiceProvider( // provide timer service to all widgets of your app
          service: timerService,
          child: MyApp(),
        ),
      );
    }
    
    class TimerService extends ChangeNotifier {
      Stopwatch _watch;
      Timer _timer;
    
      Duration get currentDuration => _currentDuration;
      Duration _currentDuration = Duration.zero;
    
      bool get isRunning => _timer != null;
    
      TimerService() {
        _watch = Stopwatch();
      }
    
      void _onTick(Timer timer) {
        _currentDuration = _watch.elapsed;
    
        // notify all listening widgets
        notifyListeners();
      }
    
      void start() {
        if (_timer != null) return;
    
        _timer = Timer.periodic(Duration(seconds: 1), _onTick);
        _watch.start();
    
        notifyListeners();
      }
    
      void stop() {
        _timer?.cancel();
        _timer = null;
        _watch.stop();
        _currentDuration = _watch.elapsed;
    
        notifyListeners();
      }
    
      void reset() {
        stop();
        _watch.reset();
        _currentDuration = Duration.zero;
    
        notifyListeners();
      }
    
      static TimerService of(BuildContext context) {
        var provider = context.inheritFromWidgetOfExactType(TimerServiceProvider) as TimerServiceProvider;
        return provider.service;
      }
    }
    
    class TimerServiceProvider extends InheritedWidget {
      const TimerServiceProvider({Key key, this.service, Widget child}) : super(key: key, child: child);
    
      final TimerService service;
    
      @override
      bool updateShouldNotify(TimerServiceProvider old) => service != old.service;
    }
    
    class MyApp extends StatelessWidget {
      @override
      Widget build(BuildContext context) {
        return MaterialApp(
          title: 'Service Demo',
          home: MyHomePage(),
        );
      }
    }
    
    class MyHomePage extends StatefulWidget {
      @override
      _MyHomePageState createState() => _MyHomePageState();
    }
    
    class _MyHomePageState extends State<MyHomePage> {
      @override
      Widget build(BuildContext context) {
        var timerService = TimerService.of(context);
        return Scaffold(
          appBar: AppBar(),
          body: Center(
            child: AnimatedBuilder(
              animation: timerService, // listen to ChangeNotifier
              builder: (context, child) {
                // this part is rebuilt whenever notifyListeners() is called
                return Column(
                  mainAxisAlignment: MainAxisAlignment.center,
                  children: <Widget>[
                    Text('Elapsed: ${timerService.currentDuration}'),
                    RaisedButton(
                      onPressed: !timerService.isRunning ? timerService.start : timerService.stop,
                      child: Text(!timerService.isRunning ? 'Start' : 'Stop'),
                    ),
                    RaisedButton(
                      onPressed: timerService.reset,
                      child: Text('Reset'),
                    )
                  ],
                );
              },
            ),
          ),
        );
      }
    }