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

178

Solution 1

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);
            },
          ),
        ],
      ),
    );
  }
}

Solution 2

You can use the Timer class to run a function after a set Duration. It doesn't give you the time remaining, but you can calculate it yourself.

Here is a quick implementation I put together:

import 'dart:async';

import 'package:flutter/material.dart';

void main() async {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      theme: ThemeData.light(),
      home: const Scaffold(
        body: Center(
          child: Countdown(),
        ),
      ),
    );
  }
}

class Countdown extends StatefulWidget {
  const Countdown({Key? key}) : super(key: key);

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

class _CountdownState extends State<Countdown> {
  bool active = false;
  Timer? timer;
  Timer? refresh;
  Stopwatch stopwatch = Stopwatch();
  Duration duration = const Duration(seconds: 5);

  _CountdownState() {
    // this is just so the time remaining text is updated
    refresh = Timer.periodic(
        const Duration(milliseconds: 100), (_) => setState(() {}));
  }

  void start() {
    setState(() {
      active = true;
      timer = Timer(duration, () {
        stop();
        onCountdownComplete();
      });
      stopwatch
        ..reset()
        ..start();
    });
  }

  void stop() {
    setState(() {
      active = false;
      timer?.cancel();
      stopwatch.stop();
    });
  }

  void onCountdownComplete() {
    showDialog(
      context: context,
      builder: (context) => const AlertDialog(
        title: Text('Countdown was not stopped!'),
      ),
    );
  }

  int secondsRemaining() {
    return duration.inSeconds - stopwatch.elapsed.inSeconds;
  }

  @override
  void dispose() {
    timer?.cancel();
    refresh?.cancel();
    stopwatch.stop();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Column(
      mainAxisSize: MainAxisSize.min,
      children: [
        if (active) Text(secondsRemaining().toString()),
        if (active)
          TextButton(onPressed: stop, child: const Text('Stop'))
        else
          TextButton(onPressed: start, child: const Text('Start')),
      ],
    );
  }
}
Share:
178
killerbird
Author by

killerbird

Updated on January 03, 2023

Comments

  • killerbird
    killerbird over 1 year

    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;
          }
        });
      }
    }
    
    • Yeasin Sheikh
      Yeasin Sheikh over 2 years
      Can you include your code-snippet that you've tried so far
    • killerbird
      killerbird over 2 years
      @YeasinSheikh check!
  • Yeasin Sheikh
    Yeasin Sheikh over 2 years
    Does refresh timer can be nested having multiple click while we can't cancel the future?
  • mmcdon20
    mmcdon20 over 2 years
    I'm not sure I understood what you are asking. I only ever create one instance of the refresh timer in the constructor, (and it gets cancelled on dispose). There is a different instance of a timer that gets created every time you click on the "Start" button. And when you click on the "Start" button it is immediately replaced with the "Stop" button which prevents you from clicking on "Start" multiple times in succession.
  • Yeasin Sheikh
    Yeasin Sheikh over 2 years
    I think you can test putting something on body of Timer.periodic, I've tried this way but failed because Future cant be canceled as much as I am aware of, you can check this
  • mmcdon20
    mmcdon20 over 2 years
    I don't think I'm following you. When you press the "Stop" button I am cancelling a Timer object not a Future object. You can cancel a Timer. I'm not using any Future objects in my solution.
  • Yeasin Sheikh
    Yeasin Sheikh over 2 years
    Yes you are right, we are just following different way