Flutter可取消未来任务插件cancelable_future的使用

Flutter可取消未来任务插件cancelable_future的使用

注意事项

切勿在生产环境中使用此插件。该代码仅用于学术研究。

什么是它?

这个插件展示了在Dart中创建可取消的Future的可能性。虽然可以实现,但并不推荐这样做。以下是详细说明。

背景

经常遇到的任务是取消Future的执行。然而,Future的架构假定操作的原子性,并不支持从外部取消代码的执行。如果开发者编写了以下代码:

Future<SomeClass> someMethod() async {
  ...
}

他必须确保这段代码不会在任何地方被任意中断。如果开发者编写了以下代码:

await future;

他必须确保future要么返回结果,要么抛出异常。这是Future的架构。这可能是最佳选项之一。或许最好是在Future的架构中直接添加一个取消方法。但是,现在要添加取消Future的功能将引入混乱到所有现有的Dart代码中。为了向代码中添加取消功能,开发者必须同意。就像签署了一份合同。这种可能性确实存在。它是异步Stream的生成器async*

Stream<SomeClass> f() async* {
  try {
    yield someResult;
    ...
    yield await someFuture;
    ...
    yield* someStream;
  } finally {
    ..
  }
}

这段代码可以在计算其值的任何yield后被外部终止。(await someFuture仍然是一个原子不可分割的操作。即使在yield之前操作被取消,someFuture也会执行其代码以完成)代码将被取消且不会继续下一步。不会有异常抛出。但是finally块会被执行。底层看起来像这样:

Stream<SomeClass> f() async* {
  try {
    yield someResult;
    // 1. value = someResult
    // 2. if (canceled) return
    // 3. send value
    ...
    yield await someFuture;
    // 1. value = await someFuture
    // 2. if (canceled) return
    // 3. send value
    ...
    yield* someStream;
    // 1. subscription = listen someStream
    // 2. if (canceled)
    //      cancel subscription
    //      return
    // 3. wait for someStream to complete
  } finally {
    ..
  }
}

因此,Dart已经提供了取消异步操作所需的一切。但是,FutureStream更清晰和方便使用,尤其是在我们谈论的是单个结果而不是一系列结果时。因此出现了CancelableOperation。“一个可以取消的异步操作”——正如文档所述。 CancelableOperation没有命名为CancelableFuture是为了避免混淆开发者。Future不能被取消,而某些异步操作似乎可以。但实际上,除非在内部实现,否则没有任何异步操作可以被取消。

Future<SomeClass> f() async {
  await ....;
  await ....;
  await ....;
  await ....;
}

你可以选择不在代码执行完成并返回值(例如null)或抛出异常之前等待代码执行,比如通过添加timeout,从而创造一种取消异步操作的幻觉。但是指定的代码将继续运行,尽管没有人需要它的结果。它仍然会继续访问网络、解析JSON、保存值到存储中并创建其他副作用。开发者会对程序的意外行为感到惊讶,或者应用程序由于某种原因变慢。

由此得出的重要结论是Future是原子性的,不能从外部取消。这是Dart团队的根本架构决定。没有任何外部决策可以改变这一点。有些人习惯了在这种情况下生活。有人转向async*。有人梦想着在Dart中出现可取消的Future

可取消的Future不能通过添加cancel方法来实现。但是只有当有一种新的架构解决方案时,开发人员才能明确表示他的代码准备好被取消。例如,它可以看起来像这样(注意我发明的新关键字async cancelable):

CancelableFuture<SomeClass> f() async cancelable {
  final SomeResource resource1 = ....;
  late final SomeResource resource2;

  await safeAsyncCodeWithoutExceptions;

  try {
    resource2 = ....;
    await someFuture1;
    await someFuture2;
    await someFuture3;
  } catch (e,s) {
    ...
  } finally {
    resource2.dispose();
  }
  resource1.dispose();
}

在这种情况下,开发者必须意识到代码可能在await safeAsyncCodeWithoutExceptions处被中断,而不仅仅是await someFuture1await someFuture2await someFuture3。然后resource1永远不会被释放,会出现资源泄漏。这是一个开发者的错误,但通过async cancelable这句话,他签了一份合同,表明他对自己犯下的错误负责。但如果开发者编写正常的async代码,他并没有承诺这种行为——他的代码应该肯定完成,resource1也应该被释放。

了解有关创建可取消的Future的问题: https://github.com/dart-lang/sdk/issues/1806

这个包做了什么?

这是一个黑客技术,为Dart添加了一个可取消的Future。你可以尝试使用具有可取消FutureFuture

这里没有async cancelable合同,所以你现在实际上可以在任何await处取消任何async代码。这是通过在Zone中可用的定时器创建钩子实现的。这并不能像yield那样取消await。但是你实际上可以通过取消所有创建的定时器来中止代码执行,并调用then方法传递的Future完成处理程序,当你写await时隐式调用它。你可以通过传递值给onValue处理程序来“完成”Future,或者通过调用onError处理程序来“抛出”异常。但是因为我们无法获取未知类T的已知值来生成通用的Future<T>,所以我们只能“抛出”异常AsyncCancelException。最终,我们可以取消任何级别的异步代码嵌套中的await,但是原始的CancelableFuture本身会在取消时抛出异常。如果你不需要异常,可以使用orNull属性或onCancel方法。

即使你最终决定在工作项目中使用这个包(我不建议这样做,因为它破坏了await逻辑),记住你只能在你的async代码中使用自己的async方法。第三方async方法你无法控制,它们不知道你的实验,当你取消它们时,它们无法释放它们使用的资源。

仅用于学术目的!

使用方法

Future<void> someOperation(int i) async {
  try {
    print('operation $i 0%');

    await Future<void>.delayed(const Duration(milliseconds: 100));
    print('operation $i 25%');

    await Future<void>.delayed(const Duration(milliseconds: 100));
    print('operation $i 50%');

    await Future<void>.delayed(const Duration(milliseconds: 100));
    print('operation $i 75%');

    await Future<void>.delayed(const Duration(milliseconds: 100));
    print('operation $i 100%');
  } finally {
    print('operation $i finally');
  }
}

Future<void> someLongOperation() async {
  try {
    print('operation 1');
    await someOperation(1);

    print('operation 2');
    await someOperation(2);

    print('operation 3');
    await someOperation(3);

    print('operation 4');
    await someOperation(4);
  } finally {
    print('operations finally');
  }
}

final f1 = CancelableFuture(() async {
  try {
    await someLongOperation();

    return 'result';
  } finally {
    print('main finally');
  }
});

final cancelfuture = Future<void>.delayed(
  const Duration(milliseconds: 650),
  () async {
    print('--- cancel ---');
    await f1.cancel();
    print('--- really canceled ---');
  },
);

print('result: ${await f1.orNull}');
print('result: ${await f1.onCancel(() => 'canceled')}');
try {
  print(await f1);
} on AsyncCancelException catch (error, stackTrace) {
  print('exception: [${error.runtimeType}] $error');
  if (printStackTrace) {
    print(Chain.forTrace(stackTrace).terse);
  }
}
await cancelfuture;

输出结果如下:

operation 1
operation 1 0%
operation 1 25%
operation 1 50%
operation 1 75%
--- cancel ---
operation 1 100%
operation 1 finally
operation 2
operation 2 0%
operation 2 finally
operations finally
main finally
result: null
result: canceled
exception: [AsyncCancelException] Async operation canceled
--- really canceled ---

更多关于Flutter可取消未来任务插件cancelable_future的使用的实战教程也可以访问 https://www.itying.com/category-92-b0.html

1 回复

更多关于Flutter可取消未来任务插件cancelable_future的使用的实战系列教程也可以访问 https://www.itying.com/category-92-b0.html


在Flutter中,cancelable_future 是一个用于创建可取消未来任务(Future)的插件。它允许你在任务执行过程中根据需求取消该任务,从而避免不必要的资源消耗或处理逻辑。下面是一个关于如何使用 cancelable_future 插件的示例代码。

首先,你需要在你的 pubspec.yaml 文件中添加该插件的依赖:

dependencies:
  flutter:
    sdk: flutter
  cancelable_future: ^x.y.z  # 替换为最新版本号

然后,运行 flutter pub get 来安装依赖。

接下来是一个完整的 Flutter 应用程序示例,展示了如何使用 cancelable_future 来创建和取消一个未来任务:

import 'package:flutter/material.dart';
import 'package:cancelable_future/cancelable_future.dart';

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Cancelable Future Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: CancelableFutureDemo(),
    );
  }
}

class CancelableFutureDemo extends StatefulWidget {
  @override
  _CancelableFutureDemoState createState() => _CancelableFutureDemoState();
}

class _CancelableFutureDemoState extends State<CancelableFutureDemo> {
  bool _isRunning = false;
  bool _isCanceled = false;
  String _result = '';

  void _startTask() async {
    setState(() {
      _isRunning = true;
      _isCanceled = false;
      _result = '';
    });

    final cancelableFuture = CancelableFuture.run(() async {
      for (int i = 0; i < 10; i++) {
        await Future.delayed(Duration(seconds: 1));
        print('Task progress: $i');

        // 检查任务是否被取消
        if (cancelableFuture.isCanceled) {
          print('Task was canceled');
          return;
        }
      }

      print('Task completed');
      return 'Task Success';
    });

    cancelableFuture.result.then((value) {
      if (!mounted) return;
      setState(() {
        _isRunning = false;
        _result = value ?? 'Task Failed';
      });
    }).catchError((error) {
      if (!mounted) return;
      setState(() {
        _isRunning = false;
        _result = 'Task Error: $error';
      });
    });

    // 持有对取消函数的引用,以便后续调用
    final cancelFunction = cancelableFuture.cancel;

    // 提供一个按钮来取消任务(在实际应用中,你可能会根据其他条件调用这个取消函数)
    ElevatedButton(
      onPressed: () {
        if (_isRunning) {
          cancelFunction();
          setState(() {
            _isCanceled = true;
          });
        }
      },
      child: Text('Cancel Task'),
    ).showDialog(context: context);
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Cancelable Future Demo'),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Text(
              _isRunning ? (_isCanceled ? 'Task is being canceled...' : 'Task is running...') : 'Task is not running',
              style: TextStyle(fontSize: 20),
            ),
            SizedBox(height: 20),
            Text(
              _result,
              style: TextStyle(fontSize: 18),
            ),
            SizedBox(height: 20),
            ElevatedButton(
              onPressed: _startTask,
              child: Text('Start Task'),
            ),
          ],
        ),
      ),
    );
  }
}

在这个示例中,我们创建了一个简单的 Flutter 应用程序,它包含一个按钮来启动一个模拟任务。这个任务每秒打印一次进度,并在10秒后完成。我们还提供了一个取消按钮,可以在任务执行期间取消该任务。

关键点是:

  1. 使用 CancelableFuture.run 来创建一个可取消的未来任务。
  2. 在任务执行过程中定期检查 cancelableFuture.isCanceled 来确定任务是否被取消。
  3. 持有对取消函数的引用,并在需要时调用它来取消任务。

这个示例展示了如何使用 cancelable_future 插件来管理可取消的未来任务,从而增强你的 Flutter 应用程序的响应性和用户体验。

回到顶部