卷 V · 异步CH 19深度 19/24

Future / Stream / async*:对照 suspend 与 Flow

直觉过境 「suspend ≈ async/await,Flow ≈ Stream。」 改一下

对应关系是对的,但形似神不似。有四处差异会实实在在地咬你:Future 是「热」的(创建就开跑)、不可取消;Stream 默认单订阅,你监听第二次它直接抛异常。这一章逐条对照你熟的协程和 Flow,把这些坑一次踩完,再给出在 Widget 里消费异步的正确姿势。

Future 热执行不可取消单订阅 StreamFutureBuilder

Future:一次性的异步结果

Future<T> 就是「未来会有一个 T」,写法和你想的一样:

Future<List<Course>> loadCourses() async {
  final resp = await http.get(uri);          // 等待期间线程让给别人(第 18 章)
  return parseCourses(resp.body);
}

// 用
final courses = await loadCourses();
// 或链式
loadCourses().then((c) => print(c)).catchError((e) => print(e));

错误处理用普通的 try/catchawait 会把 Future 的错误变成同步抛出),或者链式的 .catchError。到这里都和 Kotlin 的 suspend 函数一一对应。差异在下面两处。

⚠ 坑一:Future 是「热」的——创建就开跑

Kotlin 里 suspend fun 不调用就什么都不发生;一个 Flowcollect 就是冷的、不执行。Dart 的 Future 相反——只要那个 async 函数被调用了,它内部的代码就已经开始跑了,不管你 awaitawait

final f = loadCourses();   // ← 网络请求此刻已经发出去了!
// ...做点别的...
final result = await f;    // await 只是「等它的结果」,不是「让它开始」

这个特性有时有用(可以先并发发起几个请求,再一起 await),但会坑到从 Kotlin 来的人:你以为「还没 await 所以还没发生」,其实早发生了。想要「按需才执行」,Dart 里对应的是 Stream(冷的)或者你自己包一个函数延迟调用。

⚠ 坑二:Future 不能取消

这是和 Kotlin 协程最大的差距。协程有结构化并发viewModelScope 一销毁,里面所有协程自动取消;一个 Job 取消,它的子协程跟着取消。Dart 的 Future 没有这套——它一旦开始,就没有内置的办法叫停。

后果很实际:用户在数据还没回来时就退出了页面,那个请求还在跑,回来后你若在已销毁的 State 上 setState,就会报错。应对办法:

// 1. await 之后检查 State 还在不在(第 6 章的 mounted)
final data = await repo.load();
if (!mounted) return;              // 页面已经没了,别往下走
setState(() => _data = data);

// 2. 自己扛一个「已取消」标志,或用支持取消的库(如 dio 的 CancelToken)
// 3. 让 Riverpod 管——Widget 销毁时它会自动暂停/回收对应的 provider(第 16 章)

最省心的其实是第 3 条:把异步交给 Riverpod,它接管了「页面没了就别更新」这件事,你就不用到处写 mounted 判断。这也是第 16 章推荐它的一个隐藏理由。

为什么 Dart 选了「热」这条路,而 Kotlin 选了「冷」?这背后是两种设计取向。Kotlin 的 Flow 冷、suspend 函数不调用不执行,好处是「描述」和「执行」分得很干净——你可以先把一条数据管道搭好、传来传去,最后才 collect 启动它,中途还能取消。Dart 的 Future 热,好处是简单直接——一个异步操作就是「一件正在发生的事」,你 await 它就是等它,没有「它还没开始」这种中间态要操心。代价就是那两个坑:你没法「先描述后启动」,也没法优雅取消。这不是谁对谁错,是两种权衡,认清它你就不会用 Kotlin 的直觉去套 Dart 的行为。

Stream:多个异步值,对应 Flow

一次性结果用 Future,随时间来一串值Stream<T>——WebSocket 消息、位置更新、传感器数据、Firestore 实时查询。它对应 Kotlin 的 Flow

// 消费
stream.listen(
  (value) => print(value),        // 每来一个值
  onError: (e) => print(e),
  onDone: () => print('结束'),
);

// 或在 async 函数里用 await for
await for (final value in stream) {
  print(value);
}

Stream 也有丰富的变换(mapwhereexpandasyncMap),和 Flow 的操作符对得上。但有一个天坑:

⚠ 坑三:默认的 Stream 是「单订阅」的

普通 Stream(single-subscription)只能被 listen 一次。你监听第二次,直接抛 Bad state: Stream has already been listened to。这和 Flow「每次 collect 都是全新一条冷流」的直觉完全相反,坑过无数人。

典型翻车:在 buildlisten 同一个 stream(build 会跑多次!),第二帧就崩。解法分两种:

  • 需要多个监听者(多个 Widget 听同一个源):用 StreamController.broadcast() 建广播流,或 stream.asBroadcastStream()
  • 别在 build 里 listen——用 StreamBuilder(下面)或 Riverpod,它们帮你管订阅生命周期。

async* 和 yield:自己造一条 Stream

async* 是「生成器版的 async」,用 yield 吐值,对应 Kotlin 的 flow { emit(...) }

Stream<int> countdown(int from) async* {
  for (var i = from; i >= 0; i--) {
    await Future.delayed(const Duration(seconds: 1));
    yield i;                       // 吐出一个值(≈ emit)
  }
}

顺带认全这组关键字,它们很规整:async 返回 Futureasync* 返回 Streamyield 吐值)、sync* 返回 Iterable(惰性同步序列,yield 吐值)。yield* 是「把另一条流/序列整个接进来」。

在 Widget 里消费异步的三种正确姿势

知道怎么产异步值,还要知道怎么在界面里安全地用。绝对不要build 里直接 awaitbuild 是同步的、且跑很多次)。三种正解:

1. FutureBuilder / StreamBuilder(原生,无需库)

FutureBuilder<List<Course>>(
  future: _future,   // ← 注意:存成 State 字段,别在 build 里现造(见下方坑四)
  builder: (context, snapshot) {
    if (snapshot.connectionState != ConnectionState.done) {
      return const Center(child: CircularProgressIndicator());
    }
    if (snapshot.hasError) return ErrorView(error: snapshot.error!);
    return CourseList(courses: snapshot.data!);
  },
)
⚠ 坑四:别在 build 里现造 Future 传给 FutureBuilder

future: loadCourses() 直接写在 build 里——因为 build 每帧跑、Future 又是热的(坑一),你会每帧发一个新请求、界面在 loading 和 done 之间闪烁。正确做法:在 initState_future = loadCourses(); 存成字段,FutureBuilder 用这个稳定的字段。这个坑极其高频,记死它。

2. Riverpod 的 AsyncValue(推荐)

把异步交给 Riverpod,配上第 3 章的模式匹配,加载/成功/失败三态一目了然,还自动处理了取消和缓存:

// provider 直接返回 Future,Riverpod 自动包成 AsyncValue
@riverpod
Future<List<Course>> courseList(Ref ref) =>
    ref.watch(courseRepositoryProvider).getCourses();

// UI 侧:三态用 switch 全覆盖
Widget build(BuildContext context, WidgetRef ref) {
  final courses = ref.watch(courseListProvider);
  return switch (courses) {
    AsyncData(:final value) => CourseList(courses: value),
    AsyncError(:final error) => ErrorView(error: error),
    _ => const Center(child: CircularProgressIndicator()),
  };
}

这就是第 16 章说的「三态开箱即用」。它比 FutureBuilder 更省心的地方:自动处理页面销毁时的取消、自动缓存结果、还能 ref.invalidate 手动刷新。

3. 手动在 State 里管(要精细控制时)

initState 发起、setState 更新、mounted 保护、dispose 里取消订阅——就是第 9 章那套生命周期。适合你需要对加载过程做复杂控制的场景。

并发发起多个请求:Future.wait 与 Future.any

进课程详情页时,你可能要同时拉「课程信息」「授课老师」「我的选课状态」三个接口。别串行 await(那是三个往返时间相加),用 Future.wait 并发发起、一起等:

// ✗ 串行:总耗时 = t1 + t2 + t3
final course = await api.getCourse(id);
final teacher = await api.getTeacher(id);
final enrolled = await api.getEnrollment(id);

// ✓ 并发:总耗时 = max(t1, t2, t3)。利用 Future 是「热」的(坑一)——
//   三个函数一调用就都开跑了,wait 只是一起等结果
final (course, teacher, enrolled) = await (
  api.getCourse(id),
  api.getTeacher(id),
  api.getEnrollment(id),
).wait;   // 记录上的 .wait,直接解构成三个变量

那个 (f1, f2, f3).wait 是较新的记录扩展写法(第 3 章的记录),比老的 Future.wait([...])(返回 List,要按下标取、类型还丢了)好用得多。注意 Future.wait 里任何一个失败,整体就失败——想要「谁失败了也不影响别人」,用 eagerError: false 或分别 try/catchFuture.any 则是「谁先回就用谁」(做超时竞速、多镜像取最快的那个)。

这对应 Kotlin 的 coroutineScope { val a = async{}; val b = async{}; a.await() + b.await() }——一样的「并发发起、汇合等待」,只是 Dart 没有作用域概念,靠 Future 的热执行天然并发。

超时、重试、防抖:三个每个 App 都要的模式

这三样是网络层的常识,Dart 都有干净的写法:

// 超时:给任何 Future 加一个上限
final data = await api.load().timeout(
  const Duration(seconds: 8),
  onTimeout: () => throw NetworkFailure(),
);

// 防抖:搜索框每敲一个字都发请求会打爆后端,等用户停手再发
Timer? _debounce;
void onSearchChanged(String q) {
  _debounce?.cancel();
  _debounce = Timer(const Duration(milliseconds: 300), () => _search(q));
}
// dispose 里记得 _debounce?.cancel();(第 9 章的对称律)

防抖对学校 App 的「搜课程 / 搜同学」这类输入即搜场景几乎必备。dio 自带超时配置(第 22 章),重试可以用它的拦截器或 retry 包——而 Riverpod 3 更省事,第 16 章说过它内置了失败自动重试。

⇄ Compose 对照 · 逐条对齐,标注差异
Kotlin 协程Dart差异
suspend funFuture + asyncFuture 热执行、不可取消
Deferred / async{}就是 Future 本身Dart 的 Future 天生就是 Deferred
Flow(冷)Stream普通 Stream 单订阅,广播要显式开
flow { emit() }async* { yield }基本一致
flowOf / asFlowStream.fromIterable一致
StateFlowRiverpod 的 Notifier.state一致(第 16 章)
coroutineScope / 结构化并发没有取消靠 mounted / 库 / Riverpod
Dispatchers.IO没有,也不需要await 即可,I/O 不阻塞线程

一句话总结迁移心态:API 长得像,但 Dart 的异步更原始、更手动——没有结构化并发替你收尾,Future 也不冷不可取消。所以「谁来负责取消/清理」这件事,从「协程作用域自动管」变成了「你(或 Riverpod)显式管」。把这个责任转移记住,其余的对应关系直接搬。

这一章的一句话

Future ≈ suspend、Stream ≈ Flow,但四处会咬人:Future 创建即执行(热)、不可取消、别在 build 里现造;Stream 默认单订阅、监听第二次就抛;而 Dart 没有结构化并发,所以「异步任务的取消与清理」要你自己扛——最省心的办法是把它交给 Riverpod 的 AsyncValue。

到这里,「等待型」的异步(网络、磁盘)讲完了——它们靠 await 让出线程就够了。但还有一种重活 await 救不了:真的在烧 CPU 的计算。下一章看 Flutter 唯一的真并行手段——Isolate,以及为什么它更像多进程而不是多线程、以及为什么你多半用不上它。