Show timer progress on a CircularProgressIndicator in flutter

4,544

There's nothing to be implemented in the getter tick, since RestartableTimer is not periodic. What you want is a much more complex thing, and RestartableTimer is not able to help you with that.

First, you need something to control the progress of the CircularProgressIndicator:

class ProgressController {
  static const double smoothnessConstant = 250;

  final Duration duration;
  final Duration tickPeriod;

  Timer _timer;
  Timer _periodicTimer;

  Stream<void> get progressStream => _progressController.stream;
  StreamController<void> _progressController = StreamController<void>.broadcast();

  Stream<void> get timeoutStream => _timeoutController.stream;
  StreamController<void> _timeoutController = StreamController<void>.broadcast();

  double get progress => _progress;
  double _progress = 0;

  ProgressController({@required this.duration})
      : assert(duration != null),
        tickPeriod = _calculateTickPeriod(duration);

  void start() {
    _timer = Timer(duration, () {
      _cancelTimers();
      _setProgressAndNotify(1);
      _timeoutController.add(null);
    });

    _periodicTimer = Timer.periodic(
      tickPeriod,
      (Timer timer) {
        double progress = _calculateProgress(timer);
        _setProgressAndNotify(progress);
      },
    );
  }

  void restart() {
    _cancelTimers();
    start();
  }

  Future<void> dispose() async {
    await _cancelStreams();
    _cancelTimers();
  }

  double _calculateProgress(Timer timer) {
    double progress = timer.tick / smoothnessConstant;

    if (progress > 1) return 1;
    if (progress < 0) return 0;
    return progress;
  }

  void _setProgressAndNotify(double value) {
    _progress = value;
    _progressController.add(null);
  }

  Future<void> _cancelStreams() async {
    if (!_progressController.isClosed) await _progressController.close();
    if (!_timeoutController.isClosed) await _timeoutController.close();
  }

  void _cancelTimers() {
    if (_timer?.isActive == true) _timer.cancel();
    if (_periodicTimer?.isActive == true) _periodicTimer.cancel();
  }

  static Duration _calculateTickPeriod(Duration duration) {
    double tickPeriodMs = duration.inMilliseconds / smoothnessConstant;
    return Duration(milliseconds: tickPeriodMs.toInt());
  }
}

Then you can implement a CircularProgressIndicator that listens to the Streams from ProgressController:

class RestartableCircularProgressIndicator extends StatefulWidget {
  final ProgressController controller;
  final VoidCallback onTimeout;

  RestartableCircularProgressIndicator({
    Key key,
    @required this.controller,
    this.onTimeout,
  })  : assert(controller != null),
        super(key: key);

  @override
  _RestartableCircularProgressIndicatorState createState() =>
      _RestartableCircularProgressIndicatorState();
}

class _RestartableCircularProgressIndicatorState
    extends State<RestartableCircularProgressIndicator> {
  ProgressController get controller => widget.controller;

  VoidCallback get onTimeout => widget.onTimeout;

  @override
  void initState() {
    super.initState();
    controller.progressStream.listen((_) => updateState());
    controller.timeoutStream.listen((_) => onTimeout());
  }

  @override
  Widget build(BuildContext context) {
    return CircularProgressIndicator(
      value: controller.progress,
    );
  }

  void updateState() => setState(() {});
}

You can also pass some of the paramers of CircularProgressIndicator to RestartableCircularProgressIndicator, so you can customize it.

A usage example:

void main() => runApp(MyApp());

class MyApp extends StatefulWidget {
  @override
  _MyAppState createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {
  ProgressController controller;

  @override
  void initState() {
    super.initState();
    controller = ProgressController(
      duration: Duration(seconds: 5),
    );
  }

  @override
  void dispose() {
    controller.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        body: Center(
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: [
              RestartableCircularProgressIndicator(
                controller: controller,
                onTimeout: () => print('timeout'),
              ),
              RaisedButton(
                onPressed: controller.start,
                child: Text('Start'),
              ),
              RaisedButton(
                onPressed: controller.restart,
                child: Text('Restart'),
              ),
            ],
          ),
        ),
      ),
    );
  }
}

I'll convert this into a library someday, but until then I cannot provide the tests and documentation to this code, so you have to study it if you want to understand what's going on here (I'm sorry...).

Share:
4,544
Gil Sand
Author by

Gil Sand

Now that you've spent that much time to click on my profile, might as well check the crazy things I write on my blog, and the blog I'm writing for RiseUp I’m really just a guy who started programming in 2014. I’m from Belgium and fluently speak French, English, Objective-C, and most importantly C#. I’ve been helped a lot, by many people, this includes the current company I work with, the company that I published my first app with, and the different trainers that thaught me programming in the first place. Now I try to give back as much as I can on StackOverflow. I like it. Otherwise I’m just a regular guy. I play games with my friends while complaining about my loved half, and complain about my friends to her to keep things balanced karma-wise. Generic stuff really. And of course : #SOreadytohelp

Updated on December 12, 2022

Comments

  • Gil Sand
    Gil Sand over 1 year

    I'm using a RestartableTimer (subclass of Timer) as a countdown timer, to "kick people out" of a form after a certain duration.

    I would like to display the progress of that timer, and I like the idea of a circular progress slowly filling up.

    I'm not showing my code because I don't really have anything to show. I have a completely static progress indicator and a working timer, in a widget (stateful or stateless, whichever works best).

    I face two issues and this is where I need help for :

    • I don't know how to check every x milliseconds for the timer progress. How can I do that? I don't need copy-pasta code, but more of "what object / which direction" should I go for?

    • The timer progress in ticks is not implemented (NotImplementedException) ; is there any way to have an equivalent somewhere else? That object works really well for me, except for that part.

    Am I SOL or is there a way to make it?

  • Gil Sand
    Gil Sand almost 5 years
    Its a really good base to what I need ; I'm comfortable enough to understand it. It'll check this out tomorrow during daytime hours. Right now I found that using a Streambuilder + a Stopwatch + Timer, in a separate class. It works but I don't find it super clean yet.