Dart 里的异步处理不只有 Future。当数据不是“一次返回就结束”,而是会持续到达、按时间推进、还可能伴随错误和结束信号时,Stream才是更合适的抽象。很多开发者一开始知道它能“监听数据”,但真正写到实时搜索、文件流读取、Flutter 状态更新这类场景时,往往会卡在单订阅与广播、冷流与热流、操作符链和资源释放这些关键点上。
这篇文章按“概念 - 特性 - 价值 - 常用 API - 设计思路”的顺序展开,保留原有示例代码,并把容易混淆的点拆开说明。读完之后,你可以更清楚地判断:一个异步场景到底该用 Future 还是 Stream,以及 Stream 在 Dart 和 Flutter 里到底强在哪里。
什么是 Stream:Dart 中的连续异步事件通道
官方定义里,Stream 是 Dart 中表示连续异步数据序列的核心对象,用来处理多个按时间顺序传递的异步事件。和 Future 只负责单次结果不同,Stream 面向的是“持续发生”的数据。
一个完整的 Stream 通常包含三类事件:
- 数据事件:例如网络响应、用户输入、状态变化
- 错误事件:例如请求超时、解析失败、运行时异常
- 完成事件:表示这条流已经结束,不会再继续发送数据
Stream 的关键价值在于解耦“数据生产”和“数据消费”。生产者只负责把事件送进流里,消费者则在合适的时机订阅并处理它们,双方不需要同步阻塞等待。
下面这个例子演示了一个持续产生消息的 Stream:
Stream<String> generateMessages() async* {
int count = 1;
while (true) {
await Future.delayed(Duration(seconds: 1));
yield "消息-$count";
count++;
}
}
这里的 async* 表示异步生成器,yield 会把每次生成的数据事件依次送入流中。这个模型非常适合消息、日志、传感器读数、股票价格等“不断更新”的数据源。
Stream 的四个核心特性
异步传递:消费者通过订阅被动接收事件
Stream 的数据传递是非阻塞的。消费者通常通过 listen 或 await for 等方式接收事件,不需要主动轮询。

例如聊天场景中新消息会随时到达,可以这样监听:
Stream<String> chatStream = generateMessages();
chatStream.listen((message) {
print('收到新消息: $message');
});
一旦有新消息到来,回调就会被触发。这种模式很适合界面更新、通知分发和实时反馈。
顺序保证:事件按 FIFO 依次处理
Stream 会按照先进先出(FIFO)的顺序传递事件,保证处理顺序与发送顺序一致。这一点在用户操作记录、状态机流转、流水线处理等场景里很重要,因为顺序错乱往往意味着业务逻辑出错。
单订阅流与广播流:监听模型完全不同
Dart 中常见的流主要分成两类:
Single-Subscription:默认类型,只允许一个监听者Broadcast:允许多个监听者同时接收事件
广播流通常用于事件分发、全局通知、多人状态同步等场景。比如:
final broadcastStream = StreamController<String>().broadcast();
broadcastStream.stream.listen((status) {
print('玩家1收到游戏状态更新: $status');
});
broadcastStream.stream.listen((status) {
print('玩家2收到游戏状态更新: $status');
});
broadcastStream.sink.add('游戏开始');
这段代码里,StreamController<String>().broadcast() 创建的是广播流,因此多个监听者都能收到同一条状态更新。
冷流与热流:数据何时开始产生
理解冷流(Cold)和热流(Hot),是搞清 Stream 行为差异的关键。
- 冷流:从监听开始才触发数据生产,每次监听都可能重新执行一遍生成逻辑,
async*生成的流通常属于这一类 - 热流:数据会持续流动,不依赖某个监听者何时加入,
StreamController创建的流更接近这种模式
这意味着监听时机本身就会影响你能收到哪些数据。做实时事件分发时,这个区别尤其重要。
为什么 Stream 很重要:典型使用价值与场景
处理持续变化的异步数据
如果数据会不断变化,Stream 比一次性返回结果的 Future 更自然。股票价格、实时消息、位置更新都属于这一类。

Stream<double> stockPriceStream = Stream.periodic(Duration(seconds: 1), (count) {
// 模拟股票价格波动
return 100 + (Random().nextDouble() * 10 - 5);
});
stockPriceStream.listen((price) {
print('当前股票价格: $price');
});
Stream.periodic 会按固定节奏推送事件,很适合定时刷新、轮询抽象和模拟实时数据流。
降低内存压力,按块处理数据
当数据量很大时,Stream 允许你边接收边处理,而不是一次把所有内容加载进内存。这对大文件、网络传输和长时间运行的服务尤其重要。
File('large_file.txt')
.openRead()
.transform(utf8.decoder)
.transform(LineSplitter())
.listen((line) => processLine(line));
这段代码按行处理文件内容,内存占用更可控,也更符合流式 IO 的实际需求。
用链式操作符搭建声明式处理管道
Stream 的另一个强项,是能把一连串数据处理规则写成清晰的管道。下面这个搜索示例很典型:
searchInput.stream
.distinct()
.where((query) => query.length > 2)
.asyncMap((query) => fetchResults(query))
.listen(updateUI);
这条链路表达得很直接:先去重,再过滤长度小于 3 的输入,随后异步拉取结果,最后更新界面。相比把逻辑拆进多层回调,这种写法更利于维护。
与 Flutter 生态天然契合
在 Flutter 里,Stream 是响应式编程的重要基础设施。状态管理、界面刷新、跨组件通信都能看到它的身影,BLoC、Riverpod 等方案也经常围绕它展开。
例如一个简单的计数器流:
class CounterBloc {
final _counterController = StreamController<int>();
int _count = 0;
Stream<int> get counter => _counterController.stream;
void increment() {
_count++;
_counterController.sink.add(_count);
}
void dispose() => _counterController.close();
}
StreamBuilder<int>(
stream: counterBloc.counter,
builder: (context, snapshot) {
return Text('Count: ${snapshot.data?? 0}');
},
)
这里业务逻辑通过 StreamController 发出状态变化,界面层通过 StreamBuilder 自动响应,职责边界比较清楚。
处理多数据源协作
当一个功能依赖多条异步数据流时,Stream 的组合能力会非常有用。比如表单校验:
final usernameStream = usernameController.stream;
final passwordStream = passwordController.stream;
Stream<bool> get isFormValid =>
StreamZip([usernameStream, passwordStream])
.map((credentials) =>
credentials[0].isNotEmpty && credentials[1].length >= 6)
.distinct();
通过 StreamZip 合并两个输入流后,可以实时计算按钮是否可提交,这比手动维护多个状态再同步判断更简洁。
常用属性与方法:哪些 API 最值得先掌握
先看常用属性
Stream 自身有一些很常见的属性:
isBroadcast:是否为广播流isEmpty:异步判断流是否为空first:获取第一个数据事件last:获取最后一个数据事件single:要求流中只有一个数据事件length:统计事件总数
其中 last、single、length 在无限流上要格外小心,因为它们可能永远等不到结束。
StreamController 侧常见的属性包括:
stream:对外暴露的输出流sink:数据入口,可添加数据、错误或关闭流isClosed:控制器是否已关闭isPaused:流是否处于暂停状态hasListener:当前是否有活跃监听者done:关闭完成时对应的Future
高频方法可以先记住这几类
创建流时,常用的工厂构造函数包括:
fromIterablefromFuturefromFuturesperiodic
数据处理阶段最常见的方法包括:
listen:订阅流并处理数据、错误、完成事件map:同步转换事件where:过滤事件asyncMap:异步转换事件handleError:处理错误事件take/skip:截取或跳过指定数量的事件distinct:去掉连续重复事件
进阶一点的还有:
expandtakeWhileskipWhiletransformpipedraincastasBroadcastStream
聚合和边缘操作则包括 contains、forEach、reduce、join、every 等。这些 API 不需要一次性全背下来,但要知道它们大致解决什么问题。
一个 Stream 的基本使用流程
实际开发中,可以把 Stream 的使用拆成四步:创建、监听、处理、关闭。

1. 创建流
常见方式包括:
- 使用
Stream.fromIterable([1, 2, 3])从集合创建 - 使用
StreamController动态推送数据 - 使用
async*生成器按需产出事件
2. 监听流
通过 listen 订阅时,需要同时考虑数据事件、错误事件和完成事件。对长生命周期对象来说,还要管理订阅取消,避免内存泄漏。
3. 处理流
处理阶段通常会用到转换、过滤和错误处理,例如 map、asyncMap、where、take、skip、handleError。
4. 关闭流并清理资源
如果使用了 StreamController,最终需要显式关闭;如果创建了订阅,也要在合适时机取消。无限流或长连接场景尤其要注意这一点。
下面是一个实时搜索的完整示例:
final searchController = StreamController<String>();
searchController.stream
.where((query) => query.isNotEmpty)
.asyncMap((query) => fetchSearchResults(query))
.listen(updateUI);
searchController.sink.add("Dart");
searchController.sink.add("Flutter");
await searchController.close();
这个例子展示了从输入到异步查询,再到界面更新的完整链路,也顺带体现了 close() 的必要性。
理解 Stream 的设计哲学,才能真正用顺手
分层结构:生产者、处理管道、消费者
Stream 的运行机制可以概括为三层:
- 生产者层:外部输入、生成器、控制器负责产出异步事件
- 处理管道:通过操作符完成转换、过滤、聚合与错误传递
- 消费者层:通过订阅消费数据,并在结束时释放资源
这套分层使异步逻辑更容易拆解,也让测试和维护更清晰。
三个核心行为模式
订阅驱动:很多流只有在存在订阅者时才真正开始工作。
背压控制:可以通过 pause/resume 控制消费节奏,减少处理不过来时的数据堆积。
错误传播:错误事件会沿着操作符链传递,直到被 handleError 捕获;如果没有处理,就可能演变成更明显的运行时问题。
它和 Future 的本质区别
Future 适合单个异步结果,完成一次就结束;Stream 适合多个异步事件,生命周期可以持续存在。
Future:单次网络请求、单次数据库查询、一次性初始化任务Stream:实时聊天、文件流式传输、表单输入变化、状态持续更新
如果你的异步问题本质上是“只等一个结果”,优先考虑 Future;如果问题是“事件会持续发生”,那就该转向 Stream。
写在最后:掌握 Stream,关键是建立数据管道思维
Stream 难的地方不在语法,而在思维方式。你需要把异步数据看成一条会持续流动的事件通道,再去思考它从哪里产生、经过哪些处理、由谁消费,以及何时结束。
一旦理解了这一点,Stream 在实时消息、文件处理、Flutter 状态更新、多输入协作这些场景里的优势就会非常明显。真正需要练熟的,不只是 listen 和 map,而是对监听模型、生命周期和资源管理的判断能力。













