Completer 全解析:实用指南

242 阅读5分钟

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具有独特的优势。不妨亲自测试一下,感受它的好处!

请持续关注更多实用技巧。祝编程愉快!