Future / Stream / async*:对照 suspend 与 Flow
对应关系是对的,但形似神不似。有四处差异会实实在在地咬你:Future 是「热」的(创建就开跑)、不可取消;Stream 默认单订阅,你监听第二次它直接抛异常。这一章逐条对照你熟的协程和 Flow,把这些坑一次踩完,再给出在 Widget 里消费异步的正确姿势。
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/catch(await 会把 Future 的错误变成同步抛出),或者链式的 .catchError。到这里都和 Kotlin 的 suspend 函数一一对应。差异在下面两处。
Kotlin 里 suspend fun 不调用就什么都不发生;一个 Flow 不 collect 就是冷的、不执行。Dart 的 Future 相反——只要那个 async 函数被调用了,它内部的代码就已经开始跑了,不管你 await 不 await:
final f = loadCourses(); // ← 网络请求此刻已经发出去了! // ...做点别的... final result = await f; // await 只是「等它的结果」,不是「让它开始」
这个特性有时有用(可以先并发发起几个请求,再一起 await),但会坑到从 Kotlin 来的人:你以为「还没 await 所以还没发生」,其实早发生了。想要「按需才执行」,Dart 里对应的是 Stream(冷的)或者你自己包一个函数延迟调用。
这是和 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 也有丰富的变换(map、where、expand、asyncMap),和 Flow 的操作符对得上。但有一个天坑:
普通 Stream(single-subscription)只能被 listen 一次。你监听第二次,直接抛 Bad state: Stream has already been listened to。这和 Flow「每次 collect 都是全新一条冷流」的直觉完全相反,坑过无数人。
典型翻车:在 build 里 listen 同一个 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 返回 Future、async* 返回 Stream(yield 吐值)、sync* 返回 Iterable(惰性同步序列,yield 吐值)。yield* 是「把另一条流/序列整个接进来」。
在 Widget 里消费异步的三种正确姿势
知道怎么产异步值,还要知道怎么在界面里安全地用。绝对不要在 build 里直接 await(build 是同步的、且跑很多次)。三种正解:
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!);
},
)
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/catch。Future.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 章说过它内置了失败自动重试。
| Kotlin 协程 | Dart | 差异 |
|---|---|---|
suspend fun | Future + async | Future 热执行、不可取消 |
Deferred / async{} | 就是 Future 本身 | Dart 的 Future 天生就是 Deferred |
Flow(冷) | Stream | 普通 Stream 单订阅,广播要显式开 |
flow { emit() } | async* { yield } | 基本一致 |
flowOf / asFlow | Stream.fromIterable | 一致 |
StateFlow | Riverpod 的 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,以及为什么它更像多进程而不是多线程、以及为什么你多半用不上它。