Completer 全解析:实用指南
欢迎关注我的微信公众号:OpenFlutter.
引言
异步编程并非只是一种最佳实践或高级技术。实际上,对于设计能即时响应用户交互,同时最大限度提高可用资源效率的Flutter应用程序而言,它至关重要。
鉴于此,Dart/Flutter集成了强大且直观的异步管理工具。诸如Future、Isolate和Stream等概念,是这一编程方式的基石,开发者借此确保用户体验流畅且响应迅速。
本文将通过一些常见且适用的用例,介绍一个相对生僻的概念:Completer。
在Dart的异步编程领域中,Completer可被视为Future的补充。它让开发者能够精准控制Future的 “完成” 时机与方式,从而在管理异步任务时具备更高的灵活性。
虽然不使用Completer也往往能够完成开发,但使用Completer通常会使代码更具模块化,可读性更佳,因而更易于维护 。
什么是Completer?
Completer是一种生成Future的方式,该Future可在后续某个时刻完成(或被拒绝)。
换言之,Completer允许我们手动控制Future的成功或失败状态。
Completer有哪些基本功能?
// 初始化
final Completer<T> completer = Completer<T>();
// 返回关联的“future”
final Future<T> future = completer.future;
// 检查“future”是否已完成
bool completer.isCompleted
// 完成一个“future”
completer.complete(...);
// 使用错误拒绝一个“future”
completer.completeError(...);
为何使用Completer很有用?
在各种场景下,实现某个结果的方式有很多种,并且完全可以避免使用Completer。
然而,对于精确同步、手动管理后续结果,以及需要完全掌控异步流程的情况而言,Completer是一种简单有效的解决方案。
为了更好地展示Completer的用途,下面来看一系列实际用例。
用例1:带超时的回调等待
在第一个示例中,假设需要调用一个外部API(无法修改其代码),该API通过回调返回响应。这种情况可能出现在使用原生插件或某个软件包时。
考虑以下API签名:
void externalFunction(Function(String result) callback);
具体用例如下:调用此API,并等待回调返回的响应,或等待超时(增加一点复杂性)。如果发生超时,函数返回错误。
以下是使用Completer的解决方案:
Future<String> callAPIWithTimeout() async {
final Completer<String> completer = Completer<String>();
Timer? timer;
// 启动超时定时器
timer = Timer(const Duration(seconds: 10), () {
// 超时 => 返回错误
if (!completer.isCompleted) {
completer.completeError("Timeout");
}
});
// 调用API
externalFunction((String result) {
timer?.cancel();
if (!completer.isCompleted) {
completer.complete(result);
}
});
return completer.future;
}
解释
- 第2行:初始化Completer,并指定Future将返回一个字符串。
- 第10 - 12行:如果超时后未收到响应,则返回错误。
- 第18行:调用API并等待响应。
- 第19行:由于收到了响应,取消定时器(以防它尚未触发超时)。
- 第20 - 22行:如果未触发超时,则返回响应。
- 第25行:返回Future,以便可以等待它。
以下是调用示例:
Future<void> _onWaitForAPIResult() async {
String result = "pending";
result = await callAPIWithTimeout().catchError((error) {
return error;
});
print("result: $result");
}
补充说明
也可以不使用Timer,而是通过Future.any实现相同的结果,如下所示。但个人认为,这样的代码可读性会稍差一些,你觉得呢?
由于initState仅在首次创建StatefulWidget时调用,此方法不会被再次调用。因此,可以通过addPostFrameCallback在该方法中显示对话框,showDialog将在构建完成后执行。
用例2:构建完成后执行操作
等待渲染完成非常有用,例如,获取Widget的精确尺寸。实现方法如下:
Future<String> callAPIWithTimeout2() async {
// 启动超时Future
final Future<String> timeoutFuture = Future.delayed(
const Duration(seconds: 10),
() => throw ("Timeout"),
);
// 启动API调用Future
final Future<String> apiCallFuture = Future(() {
final Completer<String> completer = Completer<String>();
externalFunction((String result) {
if (!completer.isCompleted) {
completer.complete(result);
}
});
return completer.future;
});
// 调用API
try {
return await Future.any([apiCallFuture, timeoutFuture]);
} catch (e) {
rethrow;
}
}
用例2:取消操作
处理取消操作是异步编程中的常见难题。Dart原生并不支持取消Future,但可以使用Completer模拟这一行为,提供中断异步操作的方法。
为了说明这一点,假设有一个下载服务,允许用户取消正在进行的下载。
class DownloadService {
final Completer<void> _downloadCompleter = Completer<void>();
Future<void> startDownload() async {
try {
// 模拟持续20秒的下载过程
await Future.delayed(const Duration(seconds: 20));
// 一切正常
if (!_downloadCompleter.isCompleted) {
_downloadCompleter.complete();
}
} catch (e) {
if (!_downloadCompleter.isCompleted) {
_downloadCompleter.completeError("Download failed: $e");
}
}
}
// 用于取消下载的方法
void cancelDownload() {
if (!_downloadCompleter.isCompleted) {
_downloadCompleter.completeError("Download was cancelled by user");
}
}
Future<void> get download => _downloadCompleter.future;
}
这里为何Completer至关重要?
Completer让我们可以用特定的错误提前结束Future,模拟取消行为。否则,就没有简单的方式来报告取消操作。
以下是调用代码示例,希望代码足够清晰易懂。
class DownloadApp extends StatefulWidget {
@override
_DownloadAppState createState() => _DownloadAppState();
}
class _DownloadAppState extends State<DownloadApp> {
final DownloadService _downloadService = DownloadService();
bool _isDownloading = false;
String _message = "Ready";
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(title: Text("Download App")),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(_message),
ElevatedButton(
child: Text("Start downloading"),
onPressed: _isDownloading ? null : _startDownload,
),
ElevatedButton(
child: Text("Cancel"),
onPressed: _isDownloading ? _cancelDownload : null,
),
],
),
),
),
);
}
void updateInfo({
required bool isDownloading,
required String message,
}) {
if (mounted) {
setState(() {
_isDownloading = isDownloading;
_message = message;
});
}
}
void _startDownload() async {
updateInfo(isDownloading: true, message: "Downloading...");
try {
await _downloadService.startDownload();
updateInfo(isDownloading: false, message: "Download success");
} catch (e) {
updateInfo(isDownloading: false, message: "Failure : $e");
}
}
void _cancelDownload() {
_downloadService.cancelDownload();
updateInfo(isDownloading: false, message: "Cancelled");
}
}
用例3:具有特定逻辑的依赖Future
假设有一个应用程序需要查询三个不同的服务。
出于性能考虑,三个服务会并发调用,但每个服务的优先级不同。
如果高优先级服务失败(发生严重错误),希望立即停止所有其他请求,并给出特定通知。对于其他服务,如果失败,仅记录错误,而不中断其他服务。在这种场景下,Completer就非常有用。
class PriorityDataService {
final Completer<List<String>> _aggregateCompleter = Completer<List<String>>();
final List<String> _dataResults = [];
final List<String> minorErrors = [];
int responseCount = 0;
// 并行调用3个服务
Future<List<String>> fetchData() async {
reset();
Future.wait(
[
_fetchHighPriorityService(),
_fetchMediumPriorityService(),
_fetchLowPriorityService(),
],
eagerError: true,
);
return _aggregateCompleter.future;
}
void reset() {
_dataResults.clear();
minorErrors.clear();
responseCount = 0;
}
// 如果此服务失败,则为严重错误
Future<void> _fetchHighPriorityService() async {
try {
_dataResults.add(await highPriorityService());
} catch (e) {
_handleCriticalError(e);
}
_handleCompletion();
}
// 普通服务
Future<void> _fetchMediumPriorityService() async {
try {
_dataResults.add(await mediumPriorityService());
} catch (e) {
_handleMinorError(e);
}
_handleCompletion();
}
Future<void> _fetchLowPriorityService() async {
try {
_dataResults.add(await lowPriorityService());
} catch (e) {
_handleMinorError(e);
}
_handleCompletion();
}
// 一旦收到3个响应,完成操作
void _handleCompletion() {
responseCount++;
if (responseCount == 3 && !_aggregateCompleter.isCompleted) {
_aggregateCompleter.complete(_dataResults);
}
}
// 记录错误
void _handleMinorError(dynamic error) {
minorErrors.add("Service error: $error");
}
// 发生严重错误时,直接用错误完成
void _handleCriticalError(dynamic error) {
if (!_aggregateCompleter.isCompleted) {
_aggregateCompleter.completeError("Critical service error: $error, minor errors: $minorErrors");
}
}
}
因此,借助Completer,可以根据错误的性质添加特定逻辑,这是使用Future.wait无法轻易实现的。
以下是调用示例:
Future<void> _onInvokeServices() async {
final PriorityDataService priorityDataService = PriorityDataService();
final List<String> result = await priorityDataService.fetchData().catchError((error) {
print("errors: $error");
return <String>[];
});
print("result: $result ---> minor errors: ${priorityDataService.minorErrors}");
}
用例4:将Completer用作任务同步的信号量
在本示例中,将探究Completer如何作为信号量,控制独立异步任务的执行。
假设有三个异步任务A、B、C。任务A的执行过程会在某一点暂停,等待其他两个任务中至少一个完成,然后使用最先完成任务的结果继续执行。
import "dart:async";
class FlexibleSemaphore {
Completer<String> _completer = Completer<String>();
Future<String> get wait => _completer.future;
void release(String message) {
_completer.complete(message);
_completer = Completer<String>(); // 允许重复使用
}
}
Future<void> taskA(FlexibleSemaphore semaphore) async {
print("Task A: Starting");
await Future.delayed(Duration(seconds: 2));
print("Task A: Waiting for signal");
String message = await semaphore.wait;
print("Task A: Resuming with message: $message");
}
Future<void> taskB(FlexibleSemaphore semaphore) async {
print("Task B: Starting");
await Future.delayed(Duration(seconds: 1));
print("Task B: Sending signal");
semaphore.release("Signal from Task B");
}
Future<void> taskC(FlexibleSemaphore semaphore) async {
print("Task C: Starting");
await Future.delayed(Duration(seconds: 3));
print("Task C: Sending signal");
semaphore.release("Signal from Task C");
}
void main() async {
FlexibleSemaphore semaphore = FlexibleSemaphore();
taskA(semaphore);
taskB(semaphore);
await Future.delayed(Duration(seconds: 4));
taskC(semaphore);
print("All tasks started");
}
此示例表明,Completer允许更精细的控制和重复使用,因为它可以在代码的多个位置完成,并且可以重置以供将来使用。这在异步任务之间存在可变依赖关系的复杂场景中特别有用。
结论
在开发中,使用Completer并非必需,但它确实是使代码更具条理性和可管理性的宝贵工具。
之所以分享这项技术,是因为它极大地简化了异步流程的管理,相信它对开发者也同样有用。
Completer并非解决异步问题的万能方法,但在管理异步任务方面,它带来了显著的灵活性和强大功能。值得将其纳入开发工具库中。
虽然还有其他方法可用,但Completer具有独特的优势。不妨亲自测试一下,感受它的好处!
请持续关注更多实用技巧。祝编程愉快!