Home > Software engineering >  How to handle "onCancel" of Timer.periodic or Stream.periodic
How to handle "onCancel" of Timer.periodic or Stream.periodic

Time:11-02

Whenever a user wants to play some audio, first I want to wait 5 seconds and every second perform an action. Whenever the user pushes play button, I call onPlay() method. My first attempt of it was this:

Timer? _timer;  // I might need to cancel timer also when the user pauses playback by pause button or something

void onPlay() async {
  int _secondsLeft = 5;
  _timer = await Timer.periodic(const Duration(seconds: 1), (_) {
    if (_secondsLeft == 0) _timer?.cancel();
    _textToSpeech.speak(_secondsLeft.toString());
    --_secondsLeft;
  });
  audio.play(_curSong);
}

This attempt was not working correctly as audio.play() run immediately after Timer was initialized, not finished. I found How to make Timer.periodic cancel itself when a condition is reached? and used one of the answers to improve my code like this:

StreamSubscription? _timer;

void onPlay() async {
  int _secondsLeft = 5;
  _timer = await Stream.periodic(const Duration(seconds: 1))
      .takeWhile((_) => _secondsLeft > 0)
      .forEach((_) {
        _textToSpeech.speak(_secondsLeft.toString());
        --_secondsLeft;
      });
  audio.play(_curSong);
}

This worked better, but if the user switches to a different song or pauses playback during the intial phase, _timer?.cancel() will only cancel the stream, so everything after it (audio.play(_curSong)) is still being run. But I would like to cancel not only the stream, but also skip the audio.play() function. So I would like to run audio.play() only after the Timer/Stream finishes properly without cancellation.

Is there any option to handle Stream.periodic or Timer.periodic cancel action ("onCancel")? Or is there a better way to handle this specific task?

CodePudding user response:

Almost there. Your await on .forEach actually returns null. So there is no StreamSubscription as far as I can see.

The callback that might be the simple solution for your problem is the optional onDone in the listen method. This won't be called when Stream is cancelled.

import 'dart:async';

void main() async {
  onPlay();

  await Future.delayed(Duration(seconds: 2));

  // Uncomment me to test it
  // _timer?.cancel();
}

StreamSubscription? _timer;

void onPlay() {
  int _secondsLeft = 5;

  _timer = Stream.periodic(const Duration(seconds: 1))
      .takeWhile((_) => _secondsLeft > 0)
      .listen((event) {
    print(_secondsLeft.toString());
    --_secondsLeft;
  }, onDone: () {
    print("It's show time!");
  });
}

CodePudding user response:

You are very close, you just need to only play the song when the timer has counted down, at the same time you cancel the timer. So:

Timer? _timer;

void onPlay() async {
  int _secondsLeft = 5;
  _timer = await Timer.periodic(const Duration(seconds: 1), (_) {
    if (_secondsLeft == 0) {
      _timer?.cancel();
      audio.play(_curSong);
    } else {
      _textToSpeech.speak(_secondsLeft.toString());
      --_secondsLeft;
    }
  });
}

(Notice that this doesn't say zero, where your original did. Instead it plays the music. If you want it to say zero, change the == 0 to < 0.)

With this way of writing it, canceling the timer stops you from playing the song.

  • Related