Serge Tahé
Serge Tahé

Reputation: 2079

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.

Upvotes: 99

Views: 72740

Answers (13)

vovahost
vovahost

Reputation: 35997

You can use CancelableOperation or CancelableCompleter to cancel signal cancellation of 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.

NOTE: the futures themselves don't check for cancellation when they reach async gaps, but the callback would let an object implementing something async set a flag or call an underlying mechanism. If you don't control the object returning the Future then you as the caller have to assume that the returned Future will run to completion no matter whether you cancel it or not.

Remember to add async Dart package.

Code gist

Upvotes: 133

user48956
user48956

Reputation: 15788

Sadly, Dart does not allow cancellation - a sad oversight. You must have your future check whether is has been asked to be cancelled:

bool isCancelled = false;
Future<void> myFut(){
   while(!isCancelled){ }

}

void cancelWorker() { isCancelled=true; }

myFut()
...

cancelWorker();


The async.CancelableOperation in other answers does not cancel the worker. It only provides a framework for you to cooperatively do the above yourself.

https://github.com/dart-lang/async/issues/264

Upvotes: 0

Andrey Gordeev
Andrey Gordeev

Reputation: 32449

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();
}

Upvotes: 52

Cyrax
Cyrax

Reputation: 827

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

Upvotes: 0

mr_mmmmore
mr_mmmmore

Reputation: 2213

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.

Upvotes: 1

Atamyrat Babayev
Atamyrat Babayev

Reputation: 890

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.

Upvotes: 0

ThinkDigital
ThinkDigital

Reputation: 3539

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.

Upvotes: 5

Murali Krishna Regandla
Murali Krishna Regandla

Reputation: 1486

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.

Upvotes: 2

Robert Sutton
Robert Sutton

Reputation: 517

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;
  }
}

Upvotes: 6

Feu
Feu

Reputation: 5780

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.

Upvotes: 9

CopsOnRoad
CopsOnRoad

Reputation: 267474

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

Upvotes: 11

Argenti Apparatus
Argenti Apparatus

Reputation: 4013

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.

Upvotes: 1

Shailen Tuli
Shailen Tuli

Reputation: 14171

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:

Upvotes: 13

Related Questions