有没有办法取消Dart的Future?

85
在Dart UI中,我有一个名为submit的按钮,用于启动一个长时间异步请求。该submit处理程序返回一个Future。接下来,将submit按钮替换为cancel按钮,以允许取消整个操作。在cancel处理程序中,我想要取消由submit处理程序返回的长时间操作。但我找不到可以这样做的方法。

10
答案可以。仅供参考:您无法取消“未来”的操作。未来不是操作,而是表示操作结果的对象。没有内置的方法可以要求未来告诉底层操作停止。这就是为什么所有解决方案都是使用除“未来”之外的其他内容。这也是Dart中未来可以共享的原因。如果任何人都可以取消大家的未来,那么您必须更加小心如何共享未来。 - lrn
13个回答

128

您可以使用CancelableOperationCancelableCompleter来取消一个Future。以下是这2个版本:

Solution 1: CancelableOperation(包括测试,您可以自己尝试):

  • 取消一个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'),
  });
});
  • 取消流

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

两个测试的输出结果都将是:

then: future result
onDone

如果我们取消注释 cancellableOperation.cancel();,那么上述两个测试都将输出:

onCancel

解决方案 2:CancelableCompleter(如果您需要更多控制)

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

输出:

isCanceled: false
isCompleted: true
then: future result
onDone

现在,如果我们取消注释cancellableOperation.cancel();,我们将得到以下输出:

onCancel
isCanceled: true
isCompleted: true

注意,如果您使用 await cancellableOperation.valueawait completer.operation,则该 future 将永远不会返回结果,并且如果操作被取消,则它将无限期地等待。这是因为 await cancellableOperation.value 相当于编写 cancellableOperation.value.then(...) 但是如果操作被取消,then() 将永远不会被调用。

记得添加 async Dart 包。

代码片段


2
赞赏您使用标准的Dart库,而不是自己发明轮子。 - Alex Semeniuk
2
那不是标准的Dart库。那是一个外部依赖项。 - Andrey Gordeev
7
关于异步编程,你提出了一个很好的观点。这会让人感到混乱,因为还有一个叫做"async"的本地包。 - gsouf
4
请注意,这不能用于延迟的 futures。它们是基于“定时器”(Timer),且内部定时器引用未保存,因此一旦回调开始执行,就无法阻止其继续执行。 - geg
14
这个答案有误导性。这些类并不取消未来本身,而是创建包装器,您可以使用这些包装器来模仿取消行为。 - nt4f04und
它实际上并不取消未来。例如,此方法一直打印直到结束。 - undefined

46

如何取消 Future.delayed

一个简单的方法是使用 Timer 替代 :)

Timer _timer;

void _schedule() {
  _timer = Timer(Duration(seconds: 2), () { 
    print('Do something after delay');
  });
}

@override
void dispose() {
  _timer?.cancel();
  super.dispose();
}

3
但是你不能在 Timer 上使用 await :( - iDecode
10
你可以让计时器(Timer)完成一个补全器(Completer),然后调用者可以等待补全器的未来结果(Future)。 - jamesdlin
@jamesdlin 哦我的天啊,非常感谢您先生分享这个。您总是以某种方式证明自己是最好的!!! - iDecode
解决问题的好方法。它并不完全取消未来,但它对于我的情况做了我需要的事情。 - Leo
1
@Unknown 你是对的。已修复。 - Andrey Gordeev
显示剩余7条评论

13
据我所知,没有一种方法可以取消一个Future。但是有一种方法可以取消一个Stream订阅,也许这可以帮助你。
在按钮上调用onSubmit将返回一个StreamSubscription对象。您可以显式存储该对象,然后调用cancel()以取消流订阅:
StreamSubscription subscription = someDOMElement.onSubmit.listen((data) {

   // you code here

   if (someCondition == true) {
     subscription.cancel();
   }
});

后来,作为对某些用户操作的响应,您可以取消订阅:


我按照你的想法去做了,效果很好。问题如下:一个DAO层返回了1000个随机数,每个随机数都是通过1000个HTTP请求由远程服务器生成的。DAO层返回了一个响应,其中包括两个列表:1000个Future<int>列表和1000个与1000个HTTP请求相关联的StreamSubscription列表。我使用futures在UI中显示随机数,并使用StreamSubscriptions取消HTTP请求。谢谢! - Serge Tahé
另外,你真的需要1000个HTTP请求吗?有没有任何理由可以使用Math.Random生成1000个随机数呢?或者只需一个HTTP请求即可获取所有数字? - Shailen Tuli
我只是想测试取消一系列Future的功能。这是我找到的测试方法。是的,我接受这个答案。 - Serge Tahé

9

我实现“取消”定时执行的一种方法是使用Timer。在这种情况下,我实际上是将其推迟了。:)

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

runMultipleTimes()方法将会按顺序被多次调用,但是只有在批处理的最后1秒时才会执行onceAtTheEndOfTheBatch


2
OP 询问如何取消 Future,而不是执行。 - Jon Scalet

9

对于那些想在Flutter中实现这一点的人,这里有一个简单的示例。

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

1
这很棒,但是Completer只能运行一次。所以你不能多次调用_submit()。有没有处理这个问题的解决方案? - Septian Dika

6

以下是我的个人意见...

class CancelableFuture {
  bool cancelled = false;
  CancelableFuture(Duration duration, void Function() callback) {
    Future<void>.delayed(duration, () {
      if (!cancelled) {
        callback();
      }
    });
  }

  void cancel() {
    cancelled = true;
  }
}

请查看被接受的答案中的CancelableOperation。 - Jon Scalet
1
@JonScalet 我认为 CancelableOperation 并不会阻止未来的回调执行,而答案则可以。尝试取消一个 Future.delayed(...),它仍然会执行 --- CancelableOperation.fromFuture(Future.delayed(Duration(seconds: 1), () => print("Finished 1")), onCancel: () => print("Cancelled"),).cancel() - geg
@geg 你说得对,它实际上并没有取消 Future 的计算/执行。 - Jon Scalet
不错...但它并不像其名称所示的那样是一个Future,因此无法等待。很遗憾,因为能够等待“Future”是我使用“Future”而不是“Timer”的主要原因之一。 - mr_mmmmore
对于可取消的延迟 future,可以查看我的答案(CancelableCompleter)。 - mr_mmmmore

4

现在,您可以使用CancelableOperation来实现此操作,该类位于pub.dev的async包中。请注意,不要将这个包混淆与内置的dart核心库dart:async,因为后者没有这个类。


1

您可以使用timeout()方法

创建一个虚拟的future:

Future<String?> _myFuture() async {
    await Future.delayed(const Duration(seconds: 10));
    return 'Future completed';
}

设置3秒超时,以便在10秒之前提前停止:

_myFuture().timeout(
      const Duration(seconds: 3),
      onTimeout: () =>
          'The process took too much time to finish. Please try again later',
);

就是这样,你取消了你的未来。


1
这并不会取消未来。它只是在经过 [timeLimit] 后停止等待这个未来。异步函数仍然会运行,只是我们不再等待它。 - undefined

1
将未来的任务从“做某事”更改为“除非已取消,否则做某事”。一种明显的实现方式是在未来的闭包中设置一个布尔标志,并在处理之前以及处理期间的几个时间点检查它。
此外,这似乎有点像一个hack,但将未来的超时设置为零似乎可以有效地取消未来。

1
似乎将future的超时设置为零并不能取消future。以下是证明这一点的代码和输出。`Future f = new Future.delayed(new Duration(seconds: 2), () => print('2 secs passed')); f.timeout(const Duration(seconds: 0), onTimeout: () { print ('timed out'); });`打印`timed out 2 secs passed`。使用catchError捕获超时或提供onTimeout参数都没有任何区别。future总是运行。 - Gazihan Alankus

1

以下是取消可等待延迟未来的解决方案

这个解决方案类似于一个可等待的计时器或者一个可取消的Future.delayed,它既像计时器一样可取消,又像Future一样可等待。

它基于一个非常简单的类,CancelableCompleter,下面是一个演示:

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
}

现在是该类的代码:

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

更完整的类运行示例可在this DartPad中找到。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接