在前两节课中,我们学习了 Future
和异步编程的核心技术,它们主要用于处理单一的异步结果 (比如一次网络请求、一次文件读写)。但在实际开发中,我们经常需要处理连续的异步事件流 ------ 比如实时聊天消息、传感器数据、文件下载进度等。今天我们要学习的 Stream
(流) ,就是 Dart 中处理这类连续数据流的核心工具。
一、Stream 与 Future 的本质区别:单值 vs 多值
在学习 Stream
之前,我们先明确它与 Future
的核心差异:
特性 | Future | Stream |
---|---|---|
结果数量 | 单一结果(或错误) | 多个连续结果(或错误) |
生命周期 | 未完成 → 完成(成功 / 失败) | 未完成 → 发送数据 → 完成 / 出错 |
典型场景 | 单次网络请求、单次文件读写 | 实时数据、事件监听、进度更新 |
用生活类比:
Future
像快递包裹:一次发送,一次接收(可能成功收到,也可能丢失)。Stream
像电视节目:持续发送帧画面,你可以一直观看,直到关闭电视(取消订阅)。
代码对比:Future 与 Stream 的行为差异
dart
// Future:返回单一结果
Future<String> fetchSingleData() async {
await Future.delayed(Duration(seconds: 1));
return "单一数据结果";
}
// Stream:返回多个连续结果
Stream<String> fetchStreamData() async* {
await Future.delayed(Duration(seconds: 1));
yield "第一个数据"; // 发送第一个结果
await Future.delayed(Duration(seconds: 1));
yield "第二个数据"; // 发送第二个结果
await Future.delayed(Duration(seconds: 1));
yield "第三个数据"; // 发送第三个结果
}
void main() async {
// 处理 Future
print("开始获取 Future 数据");
String futureResult = await fetchSingleData();
print("Future 结果:$futureResult");
// 处理 Stream
print("\n开始获取 Stream 数据");
await for (String streamResult in fetchStreamData()) {
print("Stream 结果:$streamResult");
}
print("Stream 结束");
}
// 输出顺序:
// 开始获取 Future 数据
// Future 结果:单一数据结果
//
// 开始获取 Stream 数据
// (等待 1 秒)
// Stream 结果:第一个数据
// (等待 1 秒)
// Stream 结果:第二个数据
// (等待 1 秒)
// Stream 结果:第三个数据
// Stream 结束
关键差异:
Future
用await
获取一个结果后就完成了。Stream
用await for
循环持续接收多个结果,直到流结束。
二、创建 Stream:从简单到复杂的流生成方式
Dart 提供了多种创建 Stream
的方法,适用于不同场景。
1. 从可迭代对象创建:Stream.fromIterable
如果已有一组数据,想将其作为流逐个发送,使用 Stream.fromIterable
:
dart
void main() {
// 从 List 创建 Stream
Stream<String> fruitStream = Stream.fromIterable(["苹果", "香蕉", "橙子"]);
// 监听流
fruitStream.listen((fruit) {
print("收到水果:$fruit");
});
}
// 输出:
// 收到水果:苹果
// 收到水果:香蕉
// 收到水果:橙子
这种方式适用于将静态数据转换为流,方便统一处理(比如和其他动态流用相同逻辑处理)。
2. 用异步生成器创建:async*
与 yield
最常用的创建动态流的方式是使用异步生成器函数:
- 用
async*
声明函数(表示返回Stream
) - 用
yield
发送流数据(每次yield
都会向流中添加一个数据) - 用
yield*
转发另一个流的数据(嵌套流)
dart
// 生成 1~5 的数字流,每 1 秒发送一个
Stream<int> countStream(int max) async* {
for (int i = 1; i <= max; i++) {
await Future.delayed(Duration(seconds: 1));
yield i; // 发送当前数字
}
}
// 转发另一个流,并添加前缀
Stream<String> prefixStream(Stream<int> numbers) async* {
// 用 yield* 转发数字流,并转换为字符串
yield* numbers.map((number) => "数字: $number");
}
void main() {
// 创建数字流
Stream<int> numbers = countStream(3);
// 创建带前缀的流
Stream<String> prefixedNumbers = prefixStream(numbers);
// 监听带前缀的流
prefixedNumbers.listen((data) {
print(data);
});
}
// 输出(每 1 秒一行):
// 数字: 1
// 数字: 2
// 数字: 3
3. 用 StreamController
手动控制流
对于更复杂的场景(如手动触发流数据、外部事件转换为流),使用 StreamController
:
dart
import 'dart:async';
void main() {
// 创建流控制器
final controller = StreamController<String>();
// 获取控制器的流(供外部监听)
final stream = controller.stream;
// 监听流
stream.listen(
(data) {
print("收到数据:$data");
},
onDone: () {
print("流已关闭");
},
);
// 手动添加数据到流中
controller.add("第一条消息");
controller.add("第二条消息");
// 3 秒后添加最后一条消息并关闭流
Future.delayed(Duration(seconds: 3), () {
controller.add("最后一条消息");
controller.close(); // 关闭流(必须调用,否则流会一直处于活跃状态)
});
}
// 输出:
// 收到数据:第一条消息
// 收到数据:第二条消息
// (等待 3 秒)
// 收到数据:最后一条消息
// 流已关闭
4. 其他常用创建方式
Stream.periodic
:定期发送数据(如定时器)
dart
// 每 2 秒发送当前时间
Stream<DateTime> timeStream = Stream.periodic(
Duration(seconds: 2),
(count) => DateTime.now(),
);
Stream.value
:创建只包含一个数据的流(类似 Future.value
)
dart
Stream<String> singleValueStream = Stream.value("只有一个值");
三、监听 Stream:处理数据、错误与结束事件
监听流是使用 Stream
的核心操作,通过 listen
方法可以处理三种事件:数据事件 、错误事件 、结束事件。
1. 基本监听方式
dart
Stream<int> numberStreamWithError() async* {
yield 1;
yield 2;
// 发送错误
throw Exception("中途处理过程中出错了");
yield 3; // 错误后的数据代码不会执行
}
void main() {
numberStreamWithError().listen(
// 1. 处理数据事件(必选)
(data) {
print("收到数据:$data");
},
// 2. 处理错误事件(可选)
onError: (error) {
print("捕获到错误:${error.toString()}");
},
// 3. 处理结束事件(可选)
onDone: () {
print("流已完成");
},
);
}
// 输出:
// 收到数据:1
// 收到数据:2
// 捕获到错误:Exception: 处理过程中出错了
// 流已完成
注意:流一旦发送错误,后续的 yield
会被忽略,直接触发 onDone
。
2. 用 await for
循环监听
await for
是另一种监听流的方式,语法更接近同步循环,适合处理没有错误 的流(或在外部用 try-catch
处理错误):
dart
Stream<String> messageStream() async* {
yield "Hello";
await Future.delayed(Duration(seconds: 1));
yield "World";
}
void main() async {
try {
// await for 循环会持续接收流数据,直到流结束
await for (String message in messageStream()) {
print("消息:$message");
}
print("流处理完成");
} catch (e) {
print("捕获到错误:$e");
}
}
// 输出:
// 消息:Hello
// (等待 1 秒)
// 消息:World
// 流处理完成
3. 流的两种类型:单订阅流 vs 多订阅流
Dart 中的流分为两种类型,这对监听行为有重要影响:
-
单订阅流(Single-subscription) :
- 只能被一个订阅者监听一次
- 数据是一次性的,错过就无法再获取(如网络数据流)
- 大多数流默认是单订阅流
-
多订阅流(Broadcast) :
- 可以被多个订阅者同时监听
- 新订阅者只能收到订阅后的新数据(不会收到历史数据)
- 适合事件通知(如按钮点击、状态变化)
将单订阅流转换为多订阅流:
dart
// 定义countStream函数:生成从1到max的数字流,每秒发送一个
Stream<int> countStream(int max) async* {
for (int i = 1; i <= max; i++) {
await Future.delayed(Duration(seconds: 1)); // 每秒发送一个数字
yield i; // 发送当前数字到流中
}
}
void main() {
// 创建单订阅流
Stream<int> singleStream = countStream(3);
// 转换为多订阅流(广播流)
Stream<int> broadcastStream = singleStream.asBroadcastStream();
// 第一个订阅者
broadcastStream.listen((data) => print("订阅者 1: $data"));
// 延迟 1.5 秒后添加第二个订阅者
Future.delayed(Duration(milliseconds: 1500), () {
broadcastStream.listen((data) => print("订阅者 2: $data"));
});
}
// 输出(每 1 秒一行):
// 订阅者 1: 1
// 订阅者 1: 2
// 订阅者 2: 2 (第二个订阅者从订阅后的数据开始接收)
// 订阅者 1: 3
// 订阅者 2: 3
四、取消订阅:避免内存泄漏
流在不使用时必须取消订阅,否则会导致内存泄漏(流控制器和订阅者持续占用资源)。
1. 用 StreamSubscription
取消订阅
listen
方法返回 StreamSubscription
对象,调用其 cancel()
方法即可取消订阅:
dart
import 'dart:async';
void main() {
// 创建一个持续发送数据的流(每 500 毫秒)
Stream<int> continuousStream = Stream.periodic(
Duration(milliseconds: 500),
(count) => count,
);
// 订阅流并获取 subscription 对象
StreamSubscription<int> subscription = continuousStream.listen((data) {
print("收到数据:$data");
});
// 3 秒后取消订阅
Future.delayed(Duration(seconds: 3), () {
print("取消订阅");
subscription.cancel(); // 取消订阅,流不再发送数据
});
}
// 输出:
// 收到数据:0
// 收到数据:1
// 收到数据:2
// 收到数据:3
// 收到数据:4
// 收到数据:5
// 取消订阅
2. 取消订阅的最佳实践
- 在
StatefulWidget
中 :在dispose
方法中取消订阅(Flutter)
dart
@override
void dispose() {
_subscription?.cancel(); // 页面销毁时取消订阅
super.dispose();
}
使用 take
限制接收数量:自动取消订阅
dart
// 只接收前 3 个数据,然后自动取消订阅
continuousStream.take(3).listen((data) {
print("收到数据:$data");
});
五、Stream 变换:处理与转换流数据
流的强大之处在于可以通过变换操作(如过滤、映射、合并)处理数据,类似集合的操作链。
1. 常用变换方法
map
:转换数据类型
dart
Stream<int> numbers = countStream(3);
Stream<String> stringNumbers = numbers.map((n) => "数字 $n");
where
:过滤数据
dart
Stream<int> numbers = countStream(5);
Stream<int> evenNumbers = numbers.where((n) => n % 2 == 0); // 只保留偶数
take
/skip
:取前 N 个 / 跳过前 N 个
dart
Stream<int> numbers = countStream(5);
numbers.take(3).listen(print); // 输出 1,2,3
numbers.skip(2).listen(print); // 输出 3,4,5
expand
:将单个数据展开为多个数据
dart
Stream<int> numbers = countStream(2);
// 将每个数字展开为 [n, n*10]
Stream<int> expanded = numbers.expand((n) => [n, n * 10]);
expanded.listen(print); // 输出 1,10,2,20
2. 链式变换示例
dart
void main() {
// 生成 1~10 的数字流
Stream<int> numbers = Stream.fromIterable(List.generate(10, (i) => i + 1));
// 链式变换:过滤偶数 → 乘以 10 → 转换为字符串
numbers
.where((n) => n % 2 == 0) // 保留偶数:2,4,6,8,10
.map((n) => n * 10) // 乘以 10:20,40,60,80,100
.map((n) => "结果: $n") // 转换为字符串
.listen((data) => print(data));
}
// 输出:
// 结果: 20
// 结果: 40
// 结果: 60
// 结果: 80
// 结果: 100
六、实际应用场景
Stream
在实际开发中应用广泛,尤其是以下场景:
- 实时数据更新:
dart
// 模拟实时股票价格更新
Stream<double> stockPriceStream(String symbol) async* {
double price = 100.0;
while (true) {
await Future.delayed(Duration(seconds: 1));
// 随机波动价格
price += (Random().nextDouble() - 0.5) * 2;
yield price;
}
}
- 文件下载进度:
dart
// 模拟文件下载进度(0% ~ 100%)
Stream<int> downloadProgress() async* {
for (int progress = 0; progress <= 100; progress += 5) {
await Future.delayed(Duration(milliseconds: 300));
yield progress;
}
}
- 事件监听:
dart
// 将按钮点击事件转换为流(Flutter 示例)
StreamController<void> _buttonClicks = StreamController<void>();
// 按钮点击时调用
void _onButtonTap() {
_buttonClicks.add(null); // 发送点击事件
}
// 监听点击事件
_buttonClicks.stream.listen((_) {
print("按钮被点击了");
});