Search code examples
flutterdartcounter

Dart - execute a function after x seconds unless cancelled by event


I am currently writing an app using Flutter and Dart. On a button onPressed event I would like to invoke an action that executes after timeLeft seconds unless it is cancelled by correctly entering a pin. Additionally, I would like to use the value timeLeft in a Text widget.

This would require a structure with the following functionality:

  • executing a function after an x amount of seconds
  • this function should execute unless some event (e.g. entering a pin correctly) has occurred.
  • the timeLeft value should be accessible to be used in a Text widget and should update as the timer progresses.

I am wondering how to do this according to flutter's and dart's best practices. For state management I am using the provider pattern so preferably this approach is compatible with the provider pattern.

This is what I have tried so far:

class Home extends ChangeNotifier {
  int secondsLeft = 10;

  void onPressedEmergencyButton(BuildContext context) {
    countDown();
    showDialog<void>(
      context: context,
      builder: (context) {
        return ScreenLock(
          title: Text(
              "Sending message in ${context.read<Home>().secondsLeft} seconds"),
          correctString: '1234',
          canCancel: false,
          didUnlocked: () {
            Navigator.pop(context);
          },
        );
      },
    );
  }

  void countDown() {
    Future.delayed(const Duration(seconds: 1), () {
      secondsLeft =- 1;
      notifyListeners();
      if (secondsLeft <= 0) {
        // Do something
        return;
      }
    });
  }
}

Solution

  • You can use CancelableOperation from async package.

    Simplifying code-snippet and about _cancelTimer(bool) , this bool used to tell widget about true = time end, and on cancel false like _cancelTimer(false);, rest are described on code-comments.

    class TS extends StatefulWidget {
      const TS({Key? key}) : super(key: key);
    
      @override
      State<TS> createState() => _TSState();
    }
    
    class _TSState extends State<TS> {
      Timer? _timer;
      final Duration _refreseRate = const Duration(seconds: 1);
    
      CancelableOperation? _cancelableOperation;
    
      Duration taskDuration = const Duration(seconds: 5);
    
      bool isSuccess = false;
    
      _initTimer() {
        if (_cancelableOperation != null) {
          _cancelTimer(false);
        }
    
        _cancelableOperation = CancelableOperation.fromFuture(
          Future.delayed(Duration.zero),
        ).then((p0) {
          _timer = Timer.periodic(_refreseRate, (timer) {
            setState(() {
              taskDuration -= _refreseRate;
            });
            if (taskDuration <= Duration.zero) {
              /// task complete on end of duration
    
              _cancelTimer(true);
            }
          });
        }, onCancel: () {
          _timer?.cancel();
    
          setState(() {});
        });
      }
    
      _cancelTimer(bool eofT) {
        // cancel and reset everything
        _cancelableOperation?.cancel();
        _timer?.cancel();
        _timer = null;
        taskDuration = const Duration(seconds: 5);
        isSuccess = eofT;
        setState(() {});
      }
    
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          body: Center(
            child: Column(
              mainAxisSize: MainAxisSize.min,
              children: [
                if (isSuccess)
                  Container(
                    height: 100,
                    width: 100,
                    color: Colors.green,
                  ),
                if (_timer != null)
                  Text("${taskDuration.inSeconds}")
                else
                  const Text("init Timer"),
              ],
            ),
          ),
          floatingActionButton: Row(
            mainAxisSize: MainAxisSize.min,
            children: [
              FloatingActionButton(
                child: const Text("init"),
                onPressed: () {
                  _initTimer();
                },
              ),
              FloatingActionButton(
                child: const Text("Cancel"),
                onPressed: () {
                  _cancelTimer(false);
                },
              ),
            ],
          ),
        );
      }
    }