dart学习第 15 节:Stream—— 处理连续数据流

在前两节课中,我们学习了 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 结束

关键差异:

  • Futureawait 获取一个结果后就完成了。
  • Streamawait 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 在实际开发中应用广泛,尤其是以下场景:

  1. 实时数据更新
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;
  }
}
  1. 文件下载进度
dart 复制代码
// 模拟文件下载进度(0% ~ 100%)
Stream<int> downloadProgress() async* {
  for (int progress = 0; progress <= 100; progress += 5) {
    await Future.delayed(Duration(milliseconds: 300));
    yield progress;
  }
}
  1. 事件监听
dart 复制代码
// 将按钮点击事件转换为流(Flutter 示例)
StreamController<void> _buttonClicks = StreamController<void>();

// 按钮点击时调用
void _onButtonTap() {
  _buttonClicks.add(null); // 发送点击事件
}

// 监听点击事件
_buttonClicks.stream.listen((_) {
  print("按钮被点击了");
});
相关推荐
叽哥18 分钟前
dart学习第 20 节:错误处理与日志 —— 让程序更健壮
flutter·dart
TralyFang1 小时前
flutter key:ValueKey、ObjectKey、UniqueKey、GlobalKey的使用场景
flutter
叽哥2 小时前
dart学习第 19 节:元数据与反射 —— 代码的 “自我描述”
flutter·dart
w_y_fan3 小时前
Flutter中蓝牙开发:flutter_blue_plus的应用理解
flutter
LZQ <=小氣鬼=>4 小时前
Flutter简单讲解
flutter
来来走走5 小时前
Flutter开发 StatelessWidget与StatefulWidget基本了解
android·flutter
LZQ <=小氣鬼=>6 小时前
Flutter 事件总线 Event Bus
flutter·dart·事件总线·event bus
天岚9 小时前
温故知新-SchedulerBinding
flutter
0wioiw011 小时前
Apple基础(Xcode⑤-Flutter-Singbox-AI提示词)
flutter·macos·xcode