2

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;
      }
    });
  }
}
killerbird
  • 23
  • 5

2 Answers2

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);
            },
          ),
        ],
      ),
    );
  }
}
Wai Ha Lee
  • 8,598
  • 83
  • 57
  • 92
Md. Yeasin Sheikh
  • 54,221
  • 7
  • 29
  • 56
1

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')),
      ],
    );
  }
}
mmcdon20
  • 5,767
  • 1
  • 18
  • 26
  • Does `refresh` timer can be nested having multiple click while we can't cancel the future? – Md. Yeasin Sheikh Jan 05 '22 at 19:31
  • 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. – mmcdon20 Jan 05 '22 at 20:19
  • 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](https://stackoverflow.com/q/17552757/10157127) – Md. Yeasin Sheikh Jan 05 '22 at 20:25
  • 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. – mmcdon20 Jan 05 '22 at 20:33
  • Yes you are right, we are just following different way – Md. Yeasin Sheikh Jan 05 '22 at 20:47