Flutter/Dart: can you run a background service with Future.delayed? - flutter

I have to run a bunch of tasks every day on a Dart/Flutter project. This is how I currently do it:
class TaskScheduler {
DateTime _lastUpdate;
bool _isRunning = false;
void launchDailyTasks() async {
//make sure tasks are not already scheduled
if (_isRunning) return;
//check last updates
if (_lastUpdate == null) {
SharedPreferences prefs = await SharedPreferences.getInstance();
final _stamp = prefs.getInt(prefsKey(AppConstants.LAST_SYNC));
if (_stamp != null) {
_lastUpdate = DateTime.fromMillisecondsSinceEpoch(_stamp);
} else {
_lastUpdate = DateTime.now();
}
}
if (_lastUpdate.isBefore(DateTime.now().add(Duration(days: 1)))) {
_runWorkersLoop();
} else {
final _delay =
DateTime.now().difference(_lastUpdate.add(Duration(days: 1)));
Timer(_delay, () => _runWorkersLoop());
}
}
void _runWorkersLoop() async {
_isRunning = true;
_startDailyTasks();
SharedPreferences prefs = await SharedPreferences.getInstance();
prefs.setInt(prefsKey(AppConstants.LAST_SYNC),
DateTime.now().millisecondsSinceEpoch);
_lastUpdate = DateTime.now();
Future.delayed(Duration(days: 1), () => _runWorkersLoop());
}
}
And so I've been wondering: is this wrong? Why should I use a package like https://pub.dev/packages/cron to do this if this works?

In reviewing your example, you use Timer() initially, and then _runWorkersLoop() implements it's own "periodic" loop by calling itself with Future.delayed(). One way to maybe simplify this is to use Timer.periodic() which you call once and until you cancel it, it will repeat.
https://api.dart.dev/stable/2.12.0/dart-async/Timer/Timer.periodic.html
Then you have a timer instance that you can check if it's running with isRunning() and you can cancel at any time with cancel().
I looked at the source for cron lib and it uses Future.microtask which is similar to Future.delayed. Generally using a lib like that will help give you:
more eyes on the code to fix any bugs versus a home grown solution
more general functionality that you might want to use later
easier to learn/understand for someone picking up your code through more examples of use available
I assume you don't have any critical timing requirements down to the millisecond with when your stuff runs, so I think you might be interested in looking at a periodic timer as mentioned above.
One thing you might want to protect is that if a bug later calls your _runWorkersLoop() function when it's already running, it will call Future.delayed() again even though one already is waiting. You don't have a way to check for existing Future.delayed() instances but with Timer.peridic() you can use the "isRunning()" to check.

Here is the improved version after #Eradicatore's comment, in case someone is interested. Works as promised.
class TaskScheduler {
Timer _timer;
/// Launch the daily tasks.
/// If [forceUpdate] is true, any currently running task will be canceled and rerun.
/// Otherwise a new task will only be started if no previous job is running.
void launchDailyTasks({bool forceUpdate = false}) async {
bool checkLastSync = true;
if (forceUpdate) {
//cancel
if (_timer != null) _timer.cancel();
checkLastSync = false;
} else {
//don't start tasks if a previous job is running
if (_timer != null && _timer.isActive) {
return;
}
}
Duration startDelay = Duration();
if (checkLastSync) {
//check last sync date to determine when to start the timer
SharedPreferences prefs = await SharedPreferences.getInstance();
int timestamp = prefs.getInt(prefsKey(AppConstants.LAST_SYNC));
if (timestamp == null) timestamp = DateTime.now().millisecondsSinceEpoch;
final difference = DateTime.now()
.difference(DateTime.fromMillisecondsSinceEpoch(timestamp));
if (difference.inDays < 1) {
//start the timer when 1 day is reached
startDelay = Duration(
seconds: Duration(days: 1).inSeconds - difference.inSeconds);
}
}
//start tasks
Future.delayed(startDelay, () {
//run first tasks immediately once
_runWorkersLoop();
//setup periodic after
_timer = Timer.periodic(Duration(days: 1), (Timer t) {
_runWorkersLoop();
});
});
}
void _runWorkersLoop() async {
_startDailyTasks();
SharedPreferences prefs = await SharedPreferences.getInstance();
prefs.setInt(prefsKey(AppConstants.LAST_SYNC),
DateTime.now().millisecondsSinceEpoch);
}
}

Related

Flutter - How to delay a function for some seconds

I have a function which returns false when the user select incorrect answer.
tappedbutton(int index) async {
final userAnswer = await userAnswer();
if (userAnswer) {
// Executing some code
}else{
ErrorSnackbar(); // This snackbar takes 2 second to close.
}
My objective is to delay calling the function for two seconds(user can click the button again , with no action triggering) after the user selects the wrong answer and to prevent the click immediatly. How can i achieve it?
You'll have to add an helper variable in the outer scope, that will indicate whether the user is on an answer cooldown or not.
The shortest solution will be:
var answerCooldownInProgress = false;
tappedbutton(int index) async {
// Ignore user taps when cooldown is ongoing
if (answerCooldownInProgress) {
return;
}
final userAnswer = await userAnswer();
if (userAnswer) {
// ...
} else {
ErrorSnackbar();
answerCooldownInProgress = true;
await Future.delayed(const Duration(seconds: 2));
answerCooldownInProgress = false;
}
}
You can use Future.delay or Timer() class to achieve that.
In order to delay a function you can do below code or use Timer() class
tappedbutton(int index) async {
await Future.delayed(Duration(seconds: 2));
}
I don't think that your goal is to delay the function.
You're trying to find a way to let the user wait until the ErrorSnackbar is gone, right?
Try this approach. It saves the time when a button was clicked the last time and cancels every button press until 2 seconds have passed.
DateTime lastPressed = DateTime(0);
tappedButton(int index) async {
DateTime now = DateTime.now();
if (lastPressed.difference(now).inSeconds < 2) {
// button pressed again in under 2 seconds, cancel action
return;
}
lastPressed = now;
final userAnswer = await userAnswer();
if (userAnswer) {
// answer correct
} else{
// answer incorrect
ErrorSnackbar();
}
}

How to send data to AudioServiceTask class which extends BackgroundAudioTask from UI

Well, I'm stuck on this problem. I have a code for audioservice (audioplayer.dart) which takes a queue to play. I'm getting the queue from playlist.dart in audioplayer.dart using ModalRoute and save in a global variable queue. Then, I initialize the AudioPlayerService. Now everything till here is fine but inside the AudioPlayerTask class which extends BackgroundAudioTask, when I try to access the variable (inside onStart) it comes out to be an empty list. I don't know where the problem is and I'm not very much familier with the BackgroundAudioTask class. Here's how it looks like:
import .....
List<MediaItem> queue = [];
class TempScreen extends StatefulWidget {
#override
_TempScreenState createState() => _TempScreenState();
}
class _TempScreenState extends State<TempScreen> {
#override
Widget build(BuildContext context) {
queue = ModalRoute.of(context).settings.arguments;
// NOW HERE THE QUEUE IS FINE
return Container(.....all ui code);
}
// I'm using this button to start the service
audioPlayerButton() {
AudioService.start(
backgroundTaskEntrypoint: _audioPlayerTaskEntrypoint,
androidNotificationChannelName: 'Audio Service Demo',
androidNotificationColor: 0xFF2196f3,
androidNotificationIcon: 'mipmap/ic_launcher',
androidEnableQueue: true,
);
AudioService.updateQueue(queue);
print('updated queue at the start');
print('queue now is $queue');
AudioService.setRepeatMode(AudioServiceRepeatMode.none);
AudioService.setShuffleMode(AudioServiceShuffleMode.none);
AudioService.play();
}
}
void _audioPlayerTaskEntrypoint() async {
AudioServiceBackground.run(() => AudioPlayerTask());
}
class AudioPlayerTask extends BackgroundAudioTask {
AudioPlayer _player = AudioPlayer();
Seeker _seeker;
StreamSubscription<PlaybackEvent> _eventSubscription;
String kUrl = '';
String key = "38346591";
String decrypt = "";
String preferredQuality = '320';
int get index => _player.currentIndex == null ? 0 : _player.currentIndex;
MediaItem get mediaItem => index == null ? queue[0] : queue[index];
// This is just a function i'm using to get song URLs
fetchSongUrl(songId) async {
print('starting fetching url');
String songUrl =
"https://www.jiosaavn.com/api.php?app_version=5.18.3&api_version=4&readable_version=5.18.3&v=79&_format=json&__call=song.getDetails&pids=" +
songId;
var res = await get(songUrl, headers: {"Accept": "application/json"});
var resEdited = (res.body).split("-->");
var getMain = jsonDecode(resEdited[1]);
kUrl = await DesPlugin.decrypt(
key, getMain[songId]["more_info"]["encrypted_media_url"]);
kUrl = kUrl.replaceAll('96', '$preferredQuality');
print('fetched url');
return kUrl;
}
#override
Future<void> onStart(Map<String, dynamic> params) async {
print('inside onStart of audioPlayertask');
print('queue now is $queue');
// NOW HERE QUEUE COMES OUT TO BE AN EMPTY LIST
final session = await AudioSession.instance;
await session.configure(AudioSessionConfiguration.speech());
if (queue.length == 0) {
print('queue is found to be null.........');
}
_player.currentIndexStream.listen((index) {
if (index != null) AudioServiceBackground.setMediaItem(queue[index]);
});
// Propagate all events from the audio player to AudioService clients.
_eventSubscription = _player.playbackEventStream.listen((event) {
_broadcastState();
});
// Special processing for state transitions.
_player.processingStateStream.listen((state) {
switch (state) {
case ProcessingState.completed:
AudioService.currentMediaItem != queue.last
? AudioService.skipToNext()
: AudioService.stop();
break;
case ProcessingState.ready:
break;
default:
break;
}
});
// Load and broadcast the queue
print('queue is');
print(queue);
print('Index is $index');
print('MediaItem is');
print(queue[index]);
try {
if (queue[index].extras == null) {
queue[index] = queue[index].copyWith(extras: {
'URL': await fetchSongUrl(queue[index].id),
});
}
await AudioServiceBackground.setQueue(queue);
await _player.setUrl(queue[index].extras['URL']);
onPlay();
} catch (e) {
print("Error: $e");
onStop();
}
}
#override
Future<void> onSkipToQueueItem(String mediaId) async {
// Then default implementations of onSkipToNext and onSkipToPrevious will
// delegate to this method.
final newIndex = queue.indexWhere((item) => item.id == mediaId);
if (newIndex == -1) return;
_player.pause();
if (queue[newIndex].extras == null) {
queue[newIndex] = queue[newIndex].copyWith(extras: {
'URL': await fetchSongUrl(queue[newIndex].id),
});
await AudioServiceBackground.setQueue(queue);
// AudioService.updateQueue(queue);
}
await _player.setUrl(queue[newIndex].extras['URL']);
_player.play();
await AudioServiceBackground.setMediaItem(queue[newIndex]);
}
#override
Future<void> onUpdateQueue(List<MediaItem> queue) {
AudioServiceBackground.setQueue(queue = queue);
return super.onUpdateQueue(queue);
}
#override
Future<void> onPlay() => _player.play();
#override
Future<void> onPause() => _player.pause();
#override
Future<void> onSeekTo(Duration position) => _player.seek(position);
#override
Future<void> onFastForward() => _seekRelative(fastForwardInterval);
#override
Future<void> onRewind() => _seekRelative(-rewindInterval);
#override
Future<void> onSeekForward(bool begin) async => _seekContinuously(begin, 1);
#override
Future<void> onSeekBackward(bool begin) async => _seekContinuously(begin, -1);
#override
Future<void> onStop() async {
await _player.dispose();
_eventSubscription.cancel();
await _broadcastState();
// Shut down this task
await super.onStop();
}
Future<void> _seekRelative(Duration offset) async {
var newPosition = _player.position + offset;
// Make sure we don't jump out of bounds.
if (newPosition < Duration.zero) newPosition = Duration.zero;
if (newPosition > mediaItem.duration) newPosition = mediaItem.duration;
// Perform the jump via a seek.
await _player.seek(newPosition);
}
void _seekContinuously(bool begin, int direction) {
_seeker?.stop();
if (begin) {
_seeker = Seeker(_player, Duration(seconds: 10 * direction),
Duration(seconds: 1), mediaItem)
..start();
}
}
/// Broadcasts the current state to all clients.
Future<void> _broadcastState() async {
await AudioServiceBackground.setState(
controls: [
MediaControl.skipToPrevious,
if (_player.playing) MediaControl.pause else MediaControl.play,
MediaControl.stop,
MediaControl.skipToNext,
],
systemActions: [
MediaAction.seekTo,
MediaAction.seekForward,
MediaAction.seekBackward,
],
androidCompactActions: [0, 1, 3],
processingState: _getProcessingState(),
playing: _player.playing,
position: _player.position,
bufferedPosition: _player.bufferedPosition,
speed: _player.speed,
);
}
AudioProcessingState _getProcessingState() {
switch (_player.processingState) {
case ProcessingState.idle:
return AudioProcessingState.stopped;
case ProcessingState.loading:
return AudioProcessingState.connecting;
case ProcessingState.buffering:
return AudioProcessingState.buffering;
case ProcessingState.ready:
return AudioProcessingState.ready;
case ProcessingState.completed:
return AudioProcessingState.completed;
default:
throw Exception("Invalid state: ${_player.processingState}");
}
}
}
This is the full code for AudioService in-case needed.
(Answer update: Since v0.18, this sort of pitfall doesn't exist since the UI and background code run in a shared isolate. The answer below is only relevant for v0.17 and earlier.)
audio_service runs your BackgroundAudioTask in a separate isolate. In the README, it is put this way:
Note that your UI and background task run in separate isolates and do not share memory. The only way they communicate is via message passing. Your Flutter UI will only use the AudioService API to communicate with the background task, while your background task will only use the AudioServiceBackground API to interact with the UI and other clients.
The key point there is that isolates do not share memory. If you set a "global" variable in the UI isolate, it will not be set in the background isolate because the background isolate has its own separate block of memory. That is why your global queue variable is null. It is not actually the same variable, because now you actually have two copies of the variable: one in the UI isolate which has been set with a value, and the other in the background isolate which has not (yet) been set with a value.
Now, your background isolate does "later" set its own copy of the queue variable to something, and this happens via the message passing API where you pass the queue from the UI isolate into updateQueue and the background isolate receive that message and stores it into its own copy of the variable in onUpdateQueue. If you were to print out the queue after this point it would no longer be null.
There is also a line in your onStart where you are attempting to set the queue, although you should probably delete that code and let the queue only be set in onUpdateQueue. You should not attempt to access the queue in onStart since your queue won't receive its value until onUpdateQueue. If you want to avoid any null pointer exception before its set, you can initialise the queue in the background isolate to an empty list, and it will eventually get replaced by a non-empty list in onUpdateQueue without ever being null.
I would also suggest you avoid making queue a global variable. Global variables are generally bad, but in this case, it may actually be confusing you into thinking that that queue variable is the same in both the UI and the background isolate when in reality each isolate will have its own copy of the variable perhaps with different values. Thus, your code will be clearer if you make two separate "local" variables. One inside the UI and one inside the background task.
One more suggestion is that you should note that the methods in the message passing API are asynchronous methods. You should wait for the audio service to start before you send messages to it, such as setting the queue. AND you should wait for the queue to be set before you try to play from the queue:
await AudioService.start(....);
// Now the service has started, it is safe to send messages.
await AudioService.updateQueue(...);
// Now the queue has been updated, it is safe to play from it.

Flutter streaming radio doesn't restart after a call

I use audioplayers to listen to a radio channel (streaming) in Flutter. It works well in the background except when the user receives a call. In this case the playing stops but doesn't restart automatically at the end of the call.
AudioPlayer audioPlayer = new AudioPlayer();
const kUrl = "http://5.39.71.159:8865/stream";
Future play() async {
int result = await audioPlayer.play(kUrl);
setState(() {
playerState = PlayerState.playing;
});
}
Future stop() async {
int result = await audioPlayer.stop();
}
How can I put my radio in pause mode when a call comes and restart the playing at the end of the call?
Try this
audioPlayer.onPlayerStateChanged.listen((AudioPlayerState s) => {
print('Current player state: $s');
setState(() => palyerState = s);
});

How to call async functions in Stream.periodic

Is it possible to call an async function inside dart:Stream.periodic function?
I tried to wrap my async function but it is not working, please see code below.
Stream.periodic(Duration(seconds: _pollingInterval), _checkConnectivity)
String _checkConnectivity(int x) async {
return await _connectionRepository.checkConnection();
}
Use asyncMap:
Stream<String> checkConnectionStream() async* {
yield* Stream.periodic(Duration(seconds: _pollingInterval), (_) {
return _connectionRepository.checkConnection();
}).asyncMap((event) async => await event);
}
I'm not too familiar with dart streams yet, but you should be able to simulate what you're trying to achieve like this:
final controller = StreamController<String>();
Timer timer;
controller.onListen = () {
timer = Timer.periodic(
_pollingInterval,
(timer) => _connectionRepository.checkConnection().then((data){
if(!controller.isClosed){
controller.add(data);
}
}),
);
};
controller.onCancel = () {
timer?.cancel();
}
return controller.stream;
The stream does not support pause and continue, though. If you want that you'd need to override the corresponding callbacks on the controller and start/stop the timer there.
Also, depending on the timing of checkConnection this can result in events in the stream being very different to _pollingInterval.

is there any way to cancel a dart Future?

In a Dart UI, I have a button submit to launch a long async request. The submit handler returns a Future. Next, the button submit is replaced by a button cancel to allow the cancellation of the whole operation. In the cancel handler, I would like to cancel the long operation. How can I cancel the Future returned by the submit handler? I found no method to do that.
You can use CancelableOperation or CancelableCompleter to cancel a future. See below the 2 versions:
Solution 1: CancelableOperation (included in a test so you can try it yourself):
cancel a future
test("CancelableOperation with future", () async {
var cancellableOperation = CancelableOperation.fromFuture(
Future.value('future result'),
onCancel: () => {debugPrint('onCancel')},
);
// cancellableOperation.cancel(); // uncomment this to test cancellation
cancellableOperation.value.then((value) => {
debugPrint('then: $value'),
});
cancellableOperation.value.whenComplete(() => {
debugPrint('onDone'),
});
});
cancel a stream
test("CancelableOperation with stream", () async {
var cancellableOperation = CancelableOperation.fromFuture(
Future.value('future result'),
onCancel: () => {debugPrint('onCancel')},
);
// cancellableOperation.cancel(); // uncomment this to test cancellation
cancellableOperation.asStream().listen(
(value) => { debugPrint('value: $value') },
onDone: () => { debugPrint('onDone') },
);
});
Both above tests will output:
then: future result
onDone
Now if we uncomment the cancellableOperation.cancel(); then both above tests will output:
onCancel
Solution 2: CancelableCompleter (if you need more control)
test("CancelableCompleter is cancelled", () async {
CancelableCompleter completer = CancelableCompleter(onCancel: () {
print('onCancel');
});
// completer.operation.cancel(); // uncomment this to test cancellation
completer.complete(Future.value('future result'));
print('isCanceled: ${completer.isCanceled}');
print('isCompleted: ${completer.isCompleted}');
completer.operation.value.then((value) => {
print('then: $value'),
});
completer.operation.value.whenComplete(() => {
print('onDone'),
});
});
Output:
isCanceled: false
isCompleted: true
then: future result
onDone
Now if we uncomment the cancellableOperation.cancel(); we get output:
onCancel
isCanceled: true
isCompleted: true
Be aware that if you use await cancellableOperation.value or await completer.operation then the future will never return a result and it will await indefinitely if the operation was cancelled. This is because await cancellableOperation.value is the same as writing cancellableOperation.value.then(...) but then() will never be called if the operation was cancelled.
Remember to add async Dart package.
Code gist
How to cancel Future.delayed
A simple way is to use Timer instead :)
Timer _timer;
void _schedule() {
_timer = Timer(Duration(seconds: 2), () {
print('Do something after delay');
});
}
#override
void dispose() {
_timer?.cancel();
super.dispose();
}
As far as I know, there isn't a way to cancel a Future. But there is a way to cancel a Stream subscription, and maybe that can help you.
Calling onSubmit on a button returns a StreamSubscription object. You can explicitly store that object and then call cancel() on it to cancel the stream subscription:
StreamSubscription subscription = someDOMElement.onSubmit.listen((data) {
// you code here
if (someCondition == true) {
subscription.cancel();
}
});
Later, as a response to some user action, perhaps, you can cancel the subscription:
For those, who are trying to achieve this in Flutter, here is the simple example for the same.
class MyPage extends StatelessWidget {
final CancelableCompleter<bool> _completer = CancelableCompleter(onCancel: () => false);
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text("Future")),
body: Column(
children: <Widget>[
RaisedButton(
child: Text("Submit"),
onPressed: () async {
// it is true only if the future got completed
bool _isFutureCompleted = await _submit();
},
),
RaisedButton(child: Text("Cancel"), onPressed: _cancel),
],
),
);
}
Future<bool> _submit() async {
_completer.complete(Future.value(_solve()));
return _completer.operation.value;
}
// This is just a simple method that will finish the future in 5 seconds
Future<bool> _solve() async {
return await Future.delayed(Duration(seconds: 5), () => true);
}
void _cancel() async {
var value = await _completer.operation.cancel();
// if we stopped the future, we get false
assert(value == false);
}
}
One way I accomplished to 'cancel' a scheduled execution was using a Timer. In this case I was actually postponing it. :)
Timer _runJustOnceAtTheEnd;
void runMultipleTimes() {
_runJustOnceAtTheEnd?.cancel();
_runJustOnceAtTheEnd = null;
// do your processing
_runJustOnceAtTheEnd = Timer(Duration(seconds: 1), onceAtTheEndOfTheBatch);
}
void onceAtTheEndOfTheBatch() {
print("just once at the end of a batch!");
}
runMultipleTimes();
runMultipleTimes();
runMultipleTimes();
runMultipleTimes();
// will print 'just once at the end of a batch' one second after last execution
The runMultipleTimes() method will be called multiple times in sequence, but only after 1 second of a batch the onceAtTheEndOfTheBatch will be executed.
my 2 cents worth...
class CancelableFuture {
bool cancelled = false;
CancelableFuture(Duration duration, void Function() callback) {
Future<void>.delayed(duration, () {
if (!cancelled) {
callback();
}
});
}
void cancel() {
cancelled = true;
}
}
There is a CancelableOperation in the async package on pub.dev that you can use to do this now. This package is not to be confused with the built in dart core library dart:async, which doesn't have this class.
Change the future's task from 'do something' to 'do something unless it has been cancelled'. An obvious way to implement this would be to set a boolean flag and check it in the future's closure before embarking on processing, and perhaps at several points during the processing.
Also, this seems to be a bit of a hack, but setting the future's timeout to zero would appear to effectively cancel the future.
The following code helps to design the future function that timeouts and can be canceled manually.
import 'dart:async';
class API {
Completer<bool> _completer;
Timer _timer;
// This function returns 'true' only if timeout >= 5 and
// when cancelOperation() function is not called after this function call.
//
// Returns false otherwise
Future<bool> apiFunctionWithTimeout() async {
_completer = Completer<bool>();
// timeout > time taken to complete _timeConsumingOperation() (5 seconds)
const timeout = 6;
// timeout < time taken to complete _timeConsumingOperation() (5 seconds)
// const timeout = 4;
_timeConsumingOperation().then((response) {
if (_completer.isCompleted == false) {
_timer?.cancel();
_completer.complete(response);
}
});
_timer = Timer(Duration(seconds: timeout), () {
if (_completer.isCompleted == false) {
_completer.complete(false);
}
});
return _completer.future;
}
void cancelOperation() {
_timer?.cancel();
if (_completer.isCompleted == false) {
_completer.complete(false);
}
}
// this can be an HTTP call.
Future<bool> _timeConsumingOperation() async {
return await Future.delayed(Duration(seconds: 5), () => true);
}
}
void main() async {
API api = API();
api.apiFunctionWithTimeout().then((response) {
// prints 'true' if the function is not timed out or canceled, otherwise it prints false
print(response);
});
// manual cancellation. Uncomment the below line to cancel the operation.
//api.cancelOperation();
}
The return type can be changed from bool to your own data type. Completer object also should be changed accordingly.
A little class to unregister callbacks from future. This class will not prevent from execution, but can help when you need to switch to another future with the same type. Unfortunately I didn't test it, but:
class CancelableFuture<T> {
Function(Object) onErrorCallback;
Function(T) onSuccessCallback;
bool _wasCancelled = false;
CancelableFuture(Future<T> future,
{this.onSuccessCallback, this.onErrorCallback}) {
assert(onSuccessCallback != null || onErrorCallback != null);
future.then((value) {
if (!_wasCancelled && onSuccessCallback != null) {
onSuccessCallback(value);
}
}, onError: (e) {
if (!_wasCancelled && onErrorCallback != null) {
onErrorCallback(e);
}
});
}
cancel() {
_wasCancelled = true;
}
}
And here is example of usage. P.S. I use provider in my project:
_fetchPlannedLists() async {
if (_plannedListsResponse?.status != Status.LOADING) {
_plannedListsResponse = ApiResponse.loading();
notifyListeners();
}
_plannedListCancellable?.cancel();
_plannedListCancellable = CancelableFuture<List<PlannedList>>(
_plannedListRepository.fetchPlannedLists(),
onSuccessCallback: (plannedLists) {
_plannedListsResponse = ApiResponse.completed(plannedLists);
notifyListeners();
}, onErrorCallback: (e) {
print('Planned list provider error: $e');
_plannedListsResponse = ApiResponse.error(e);
notifyListeners();
});
}
You could use it in situations, when language changed, and request was made, you don't care about previous response and making another request!
In addition, I really was wondered that this feature didn't come from the box.
Here's a solution to cancel an awaitable delayed future
This solution is like an awaitable Timer or a cancelable Future.delayed: it's cancelable like a Timer AND awaitable like a Future.
It's base on a very simple class, CancelableCompleter, here's a demo:
import 'dart:async';
void main() async {
print('start');
// Create a completer that completes after 2 seconds…
final completer = CancelableCompleter.auto(Duration(seconds: 2));
// … but schedule the cancelation after 1 second
Future.delayed(Duration(seconds: 1), completer.cancel);
// We want to await the result
final result = await completer.future;
print(result ? 'completed' : 'canceled');
print('done');
// OUTPUT:
// start
// canceled
// done
}
Now the code of the class:
class CancelableCompleter {
CancelableCompleter.auto(Duration delay) : _completer = Completer() {
_timer = Timer(delay, _complete);
}
final Completer<bool> _completer;
late final Timer? _timer;
bool _isCompleted = false;
bool _isCanceled = false;
Future<bool> get future => _completer.future;
void cancel() {
if (!_isCompleted && !_isCanceled) {
_timer?.cancel();
_isCanceled = true;
_completer.complete(false);
}
}
void _complete() {
if (!_isCompleted && !_isCanceled) {
_isCompleted = true;
_completer.complete(true);
}
}
}
A running example with a more complete class is available in this DartPad.
You can use timeout() method
Create a dummy future:
Future<String?> _myFuture() async {
await Future.delayed(const Duration(seconds: 10));
return 'Future completed';
}
Setting a timeout of 3 seconds to stop early from 10sec:
_myFuture().timeout(
const Duration(seconds: 3),
onTimeout: () =>
'The process took too much time to finish. Please try again later',
);
and thats it you cancel your FUTURE.
there is no way unfortunately, take a look:
import 'dart:async';
import 'package:async/async.dart';
void main(List<String> args) async {
final object = SomeTimer();
await Future.delayed(Duration(seconds: 1));
object.dispose();
print('finish program');
}
class SomeTimer {
SomeTimer() {
init();
}
Future<void> init() async {
completer
.complete(Future.delayed(Duration(seconds: 10), () => someState = 1));
print('before wait');
await completer.operation.valueOrCancellation();
print('after wait');
if (completer.isCanceled) {
print('isCanceled');
return;
}
print('timer');
timer = Timer(Duration(seconds: 5), (() => print('finish timer')));
}
Timer? timer;
int _someState = 0;
set someState(int value) {
print('someState set to $value');
_someState = value;
}
CancelableCompleter completer = CancelableCompleter(onCancel: () {
print('onCancel');
});
void dispose() {
completer.operation.cancel();
timer?.cancel();
}
}
after ten seconds you will see someState set to 1 no matter what