最后核验:2026-09-07

本篇结论

Future<T> 表示一次异步操作将来的结果,成功时得到 T,失败时得到错误。async 函数返回 Future,await 等待它完成;把 await 放进 try,才能在这一层处理异步错误。空列表是成功结果,加载失败应保留失败语义,不能统一变成空列表。

本篇围绕一次 Spark 灵感加载展开:先拿到 Future,再拿到列表,最后处理成功、空数据或格式错误。只用本地延迟模拟等待,不访问磁盘数据文件或网络,不代表已经实现持久化、网络请求或同步。

学完你能做到

  • 区分 Future<List<SparkIdea>> 与 List<SparkIdea>。
  • 声明返回数据和不返回业务数据的异步函数。
  • 预测调用、等待和继续执行的顺序。
  • 用 try、on ... catch、finally 处理异步结果。
  • 保留加载失败与空数据之间的区别。
  • 通过运行结果和退出状态验证成功与失败路径。

开始前检查

先完成 第 12 课 。本课复用 SparkIdea、parseIdea() 和 pendingIdeas(),保留以下文件的相对位置:

examples/lesson_11_functions_and_classes.dart
examples/lesson_12_collections_and_patterns.dart
examples/lesson_13_async_and_errors.dart
scripts/verify_lesson_13.dart

按 第 08 课 设置 FLUTTER_UPSTREAM_HOME,从仓库根目录确认版本:

"$FLUTTER_UPSTREAM_HOME/bin/dart" --version

本篇实际验证版本为 Dart 3.13.2。示例只依赖标准库和已有课程文件,不需要第三方包、账号或联网服务。官方文档用于核对语义,正文中的命令和代码另经本机运行验证。

01 一次加载为什么返回 Future

第 12 课的原始数据立即可用。实际应用读取数据时,结果往往稍后才能得到;调用方必须知道什么时候可以使用数据,以及失败时怎么办。

先把加载的类型边界写清楚:

类型 含义
List<SparkIdea> 已经取得的列表,可以读取长度和遍历
Future<List<SparkIdea>> 一次操作的将来结果,需要等待完成
Future<void> 可以等待操作结束,但没有供调用方使用的业务返回值

Future 完成时提供一个结果或错误;它不是持续更新列表的订阅。多次变化的事件留到第 14 课的 Stream。Future 的完成语义见 Dart 官方异步教程 。

02 用本地延迟建立最小加载函数

本例把等待时间固定为 100 毫秒,便于重复运行:

Future<List<SparkIdea>> loadIdeas(List<Object?> records) async {
  final rawIdeas = await Future<List<Object?>>.delayed(
    const Duration(milliseconds: 100),
    () => records,
  );
  return rawIdeas.map(parseIdea).toList();
}

Future.delayed 在延迟后执行回调,本例回调返回传入的 records。这不是精确定时保证,也不会读取外部数据。这里显式提供回调,与 Future.delayed API 的计算结果语义一致。

await 之后的 rawIdeas 才是列表。接下来复用第 12 课的转换:map(parseIdea) 逐条解析,toList() 真正执行转换。若某条数据格式不合法,解析抛出的错误会使 loadIdeas() 返回的 Future 以错误完成,不会返回已解析的一部分。

函数签名返回 Future<List<SparkIdea>>,函数体却直接 return 一个列表,因为 async 负责把结果交给 Future。调用方得到的仍是 Future。声明规则见 Dart 官方异步语言说明 。

本例的回调直接返回原输入列表,未制作输入快照;调用后不要在等待期间修改 records。示例中的调用方遵守这个约定,不额外引入数据所有权设计。

03 观察调用与等待的先后顺序

在完整示例的 try 中,先保留 Future,再立刻等待它:

final future = loadIdeas(records);
print('已取得 Future,尚未取得列表');
final ideas = await future;
final pending = pendingIdeas(ideas);

调用异步函数时,函数体先执行到第一个 await,随后把 Future 交回调用方。因此上面的提示可以在列表可用前打印。await future 暂停当前函数后续步骤,Future 成功完成后才继续筛选。执行顺序依据见 官方教程的执行流程 。

日常不需要观察这条分界时,可以合写为 final ideas = await loadIdeas(records);。本例拆开是为了看清「操作已发起」与「结果已取得」之间的差别。

await 让出等待期间的执行机会,不会自动创建后台线程。async 内的同步计算仍然会占用当前 isolate;给大量计算加上 async 并不能自动解决界面卡顿。事件循环与 isolate 的边界见 Dart 官方并发说明 。本课不引入并行计算。

04 在等待的位置处理错误

完整示例在命令行入口处理预期的格式错误:

print('开始加载');
try {
  final future = loadIdeas(records);
  print('已取得 Future,尚未取得列表');
  final ideas = await future;
  final pending = pendingIdeas(ideas);
  print('加载成功:${ideas.length} 条灵感,${pending.length} 条待办');
  if (pending.isEmpty) {
    print('暂无待办');
  } else {
    print('待办标题:${pending.map((idea) => idea.title).join('、')}');
  }
} on FormatException catch (error) {
  print('加载失败:${error.message}');
  exitCode = 1;
} finally {
  print('本次加载结束');
}

这段代码放在 Future<void> main(List<String> arguments) async 中。main() 需要等待加载,但没有业务结果需要返回,因此使用 Future<void>。

on FormatException 只匹配格式异常,catch (error) 取得异常对象。finally 在这次 try 正常结束或因异常离开时执行;本例只打印结束标记,不能据此判断成功。错误处理语义见 Dart 官方错误处理说明 。

捕获异常后,如果不再传播,程序可能正常结束。为让命令行调用方识别失败,本例导入 dart:io 的 exitCode 并设为 1;该赋值不立即终止进程,所以 finally 仍会执行。它属于命令行演示,不能照搬成 Flutter 页面的失败反馈。依据见 exitCode API 。

标题为空和优先级越界仍由前两课的构造函数抛出 StateError、RangeError。本例没有把所有错误都转换成相同提示,这些未匹配的错误继续向上传播,边界脚本会验证它们。

如果某一层只能记录问题、不能完成恢复,可以在 catch 中使用 rethrow 继续传播原异常。不要在底层随意 catch 后 return [],否则上层无法区分「没有灵感」和「未能取得灵感」。

05 为什么只调用、不 await 会漏掉错误

try 的作用范围不是「所有曾经在这里发起的操作」。如果只在 try 中调用 loadIdeas(records),却既不等待也不返回它,代码块可能已经结束,异步解析才失败。这个错误不会被该同步调用周围的 catch 接住。

本例必须在 try 中写 await future,让 Future 的失败在等待点进入异常处理。也不要先创建 Future,等待另一个无关操作很久以后才为它安装错误处理;错误可能在此之前就已发生。及时处理 Future 错误的说明见 Future API 。

返回 Future 给调用方,由调用方负责等待,也是一种明确的责任传递。但如果当前函数需要用自己的 catch 或 finally 处理它的完成,就应在该 try 内等待。

06 分别运行成功、空数据与失败

打开 完整示例 。模式参数最多一个,只接受 success、empty、invalid;省略时为 success。

先执行格式检查和静态分析:

"$FLUTTER_UPSTREAM_HOME/bin/dart" format --output=none --set-exit-if-changed \
  examples/lesson_13_async_and_errors.dart scripts/verify_lesson_13.dart
"$FLUTTER_UPSTREAM_HOME/bin/dart" analyze \
  examples/lesson_13_async_and_errors.dart scripts/verify_lesson_13.dart

预期无格式变更,分析显示 No issues found!。运行默认成功路径:

"$FLUTTER_UPSTREAM_HOME/bin/dart" run examples/lesson_13_async_and_errors.dart

预期输出:

开始加载
已取得 Future,尚未取得列表
加载成功:3 条灵感,2 条待办
待办标题:练习异步加载、整理 Flutter 笔记
本次加载结束

再运行空数据路径:

"$FLUTTER_UPSTREAM_HOME/bin/dart" run examples/lesson_13_async_and_errors.dart empty

预期输出:

开始加载
已取得 Future,尚未取得列表
加载成功:0 条灵感,0 条待办
暂无待办
本次加载结束

最后用字符串 '1' 代替整数优先级,触发延迟之后的格式错误:

"$FLUTTER_UPSTREAM_HOME/bin/dart" run examples/lesson_13_async_and_errors.dart invalid
echo $?

预期输出及紧接着读取的退出状态:

开始加载
已取得 Future,尚未取得列表
加载失败:灵感数据需要 title 字符串、priority 整数和 isCompleted 布尔值
本次加载结束
1

成功和空数据路径的退出状态均为 0。读取 $? 前不要插入其他命令,否则得到的是那条命令的状态。无效模式或多余参数会在加载前失败,不进入本次加载的 try/finally。

07 验证异步边界

从仓库根目录执行 验证脚本 :

"$FLUTTER_UPSTREAM_HOME/bin/dart" run scripts/verify_lesson_13.dart

预期输出 第 13 课异步边界验证通过,退出状态为零。验证函数也必须 await 被测操作,否则可能在错误发生前就宣布通过。

场景 预期结果
有效输入 等待后取得列表,标题已清洗
空输入 成功返回空列表
等待期间安排另一个微任务 微任务能在加载完成前执行
有效记录后混入 null、缺字段或错误类型 Future 以 FormatException 失败,不返回部分结果
空白标题、优先级 4 原有 StateError、RangeError 通过 Future 传播
一次失败后再次加载有效输入 后一次成功,不受前一次失败影响

脚本用标准库的 scheduleMicrotask 安排短小的独立操作,验证等待期间让出执行机会,不通过比较实际毫秒数判断通过。微任务仅用于验证,调度细节不作为本课学习要求。依据见 scheduleMicrotask API 。

三端差异

  • Track A:本课使用 Dart 3.13.2 验证本地异步加载、异常传播与命令行退出状态。
  • iOS/Android:示例不调用移动平台 API,不为每个平台重复运行;另行对 Spark 基线执行 Flutter 分析、测试、iOS Simulator 构建与 Android debug APK 回归构建。命令行验证不代表已经完成移动端加载页面或设备交互验证。
  • Track B:本机尚未安装 Flutter-OH,本课不宣称该轨道的运行、构建或插件兼容已经通过。

预期结果

  • 可以从输出分清 Future 返回和列表可用的时刻。
  • 有效数据与空数据均正常完成,格式错误有明确失败提示和非零状态。
  • 成功与已处理失败都执行结束标记。
  • 异步验证脚本实际等待结果,能识别错误类型和后续调用结果。

常见问题

async 是不是把整个函数放到后台线程

不是。本例仍在同一个 isolate 中执行,等待期间让出执行机会,恢复后继续执行同步解析。耗时计算需要单独分析,不能靠添加 async 解决。

await 会不会把整个应用停住

它暂停当前异步函数的后续步骤,其他可调度工作仍可执行。但 await 前后若执行大量同步计算,依然会占用当前 isolate。

Future 是否表示无法知道失败

不是。void 表示没有需要消费的业务返回值,仍可等待完成并捕获错误。本课的验证入口也使用 Future<void>。

finally 输出结束,是否代表成功

不是。成功、已捕获错误和继续向外传播的错误都可能经过 finally。成功要看实际结果,命令行自动化还要检查退出状态。

为什么不把加载失败统一返回空列表

这样会丢失失败语义。本例只有输入确实为空时才成功得到空列表;格式错误保留异常,交由能决定如何反馈的调用层处理。

完成检查

  • 我能区分 Future 与它完成后的值。
  • 我会为异步函数声明 Future<T> 或 Future<void>。
  • 我知道需要在 try 内等待,才能在这一层处理异步失败。
  • 我不会把空数据、失败和操作结束当成同一种状态。
  • 我已核对三种运行输出及失败退出状态。
  • 我已运行并通过异步边界验证脚本。

一手来源