系统化掌握Dart编程之异步编程(六):Stream筑基篇

前言

Stream ------ 快递分拣中心的智能传送带系统

在移动应用中,数据 就像血液一样在应用中流动:用户滑动列表时不断加载的新条目、聊天室实时刷新的消息、文件下载时跳动的进度条等,这些场景背后都隐藏着一个关键问题:如何高效管理持续产生的异步数据?

传统的Future虽然能处理单次异步操作 ,但面对连绵不断的数据流 却显得力不从心Stream正是为此而生 ------ 它像一条智能传送带 ,让数据按需流动、精准分发。然而许多开发者仅停留在简单使用listen(),却未能真正理解其设计哲学。

本文将带你穿透API表层,用系统化思维构建Stream知识体系,最终通过企业级实战案例,让你掌握"让数据流动"的艺术。

千曲 而后晓声,观千剑 而后识器。虐它千百遍 方能通晓其真意

一、基本概念

官方定义StreamDart中表示连续异步数据序列 的核心对象,用于处理多个按时间顺序传递异步事件

这些事件可以是:

  • 数据事件(Data Event :携带业务数据 (如网络响应用户输入)。
  • 错误事件(Error Event :传递异常信息 (如网络超时数据解析失败)。
  • 完成事件(Done Event标识数据流终止 (如文件读取完成)。

核心设计目标解耦数据生产与消费,允许数据在"生产者""消费者"之间按需流动,双方无需同步等待彼此。

可将想象成一个快递分拣中心的智能传送带系统:

  • 包裹 :每个数据包 (如用户操作网络响应文件内容)就像快递包裹,可能包含有效数据错误信息破损包裹)或完成信号运输结束)。
  • 传送带 :数据按固定方向流动的通道,包裹按顺序进入传送带(FIFO原则),但包裹的产生分拣是异步进行的。
  • 分拣机器人 :通过mapwhere等操作符(类似分拣规则)对包裹进行拆包检查重新打包
  • 分拣中心控制台StreamController负责启动传送带、调节流速(pause/resume)或关闭系统(close)。

深入理解

  • 包裹按需生成 :快递车(数据源)将包裹逐个放到传送带上,无需一次性堆满所有包裹(避免内存爆炸)。
  • 异步分拣 :分拣工人(消费者)无需守在传送带起点,包裹到达时会自动触发分拣动作(监听回调)。
  • 多级分拣流程 :包裹可经过多个分拣站(操作符链),依次完成过滤变形合并等操作(如stream.map(...).where(...))。

核心思想

Stream的本质是一个高度可定制的异步分拣系统 ,开发者扮演分拣中心总控师的角色,通过定义包裹来源生产者)、分拣规则操作符)、派送策略监听逻辑),实现数据从源头到终点的智能流动 。掌握Stream,就是掌握让数据像快递包裹一样高效精准有序流动的能力。


二、核心特性

2.1、异步性

  • 数据按非阻塞方式传递,消费者通过订阅listen)被动接收事件,无需主动轮询
  • 数据可能在任何时间到达,消费者通过回调函数await for处理。

2.2、序列性

  • 事件严格按先进先出FIFO)顺序传递,保证处理顺序与发送顺序一致。

2.3、多监听者支持

  • 单订阅流(Single-Subscription :仅允许一个监听者,确保数据完整性和顺序性(默认类型)。
  • 广播流(Broadcast :允许多个监听者,适用于事件广播场景(需显式声明isBroadcast: true)。

2.4、冷流(Cold)与热流(Hot

  • 冷流 :数据生成从监听时开始,每次监听会重新触发数据生产(如async*生成的流)。
  • 热流 :数据实时流动,与监听时机无关(如StreamController创建的流)。

2.5、物流类比理解

  • 包裹来源

    • 发件人(生产者) :可能是仓库(StreamController)、自动包裹生成机(async*函数),或外部快递联盟(WebSocket)。

    • 包裹产生规则

      dart 复制代码
      // 示例:模拟快递包裹生成(每秒产生一个包裹)  
      Stream<String> generatePackages() async* {  
        int count = 1;  
        while (true) {  
          await Future.delayed(Duration(seconds: 1));  
          yield "包裹-$count";  // 生成包裹,如"包裹-1", "包裹-2"  
          count++;  
        }  
      }  
  • 分拣流程

    • 单线分拣 vs 多线分拣

      • 单线分拣(Single-Subscription Stream
        一个传送带只服务一个分拣工人(监听者),确保包裹顺序严格一致(如重要文件必须按顺序签收)。
      • 多线分拣(Broadcast Stream
        传送带广播包裹,允许多个分拣工人同时处理(如多个分拣机器人同时扫描包裹条形码)。
    • 冷流 vs 热流

      类型 快递场景类比 Stream行为
      冷流 预约制快递:客户下单后,仓库才开始打包发货 每次监听时重新开始生成数据(async*生成的流)
      热流 实时物流:包裹已在运输中,新客户只能收到当前及之后的包裹 数据实时流动,与监听时机无关(StreamController创建的流)
  • 分拣控制

    • 包裹拦截subscription.pause()(暂停分拣)
    • 恢复分拣subscription.resume()
    • 终止分拣subscription.cancel()(工人离开岗位)或controller.close()(关闭分拣中心)

三、核心价值

3.1、解决持续性异步数据流的天然需求

许多场景中,数据并非一次性产生,而是持续动态按节奏生成的。例如:

  • 实时通信聊天消息WebSocket推送需要持续监听并处理 。
  • 用户交互屏幕滑动事件输入框内容变化需要实时响应。
  • 数据监控传感器数据设备状态需要周期性采集。

Stream为此类场景提供了原生支持 ,无需手动管理事件队列定时轮询,只需通过listen()订阅数据流,即可自动接收并处理异步事件。


3.2、实现资源高效利用内存安全

传统一次性异步操作(如Future)在处理大规模数据时面临挑战:

  • 内存压力 :若一次性加载全部数据(如1GB文件),可能引发内存溢出
  • 响应延迟:用户需等待所有数据加载完成才能交互。

Stream通过逐块处理数据解决这些问题:

dart 复制代码
// 逐行读取大文件,内存占用恒定  
File('large_file.txt')  
    .openRead()                  // 创建字节流  
    .transform(utf8.decoder)     // 转换为文本流  
    .transform(LineSplitter())   // 拆分为行数据流  
    .listen((line) => processLine(line)); // 逐行处理  

数据像流水一样逐块流过处理管道,每个数据块处理完成后立即释放内存,确保应用高效稳定运行。


3.3、构建声明式数据处理管道

Stream允许通过链式操作符 组合数据处理逻辑,显著提升代码可读性可维护性

搜索框优化示例

dart 复制代码
searchInput.stream  
    .distinct()                            // 去重:输入内容相同时跳过  
    .where((query) => query.length > 2)    // 过滤:至少3个字符才搜索  
    .asyncMap((query) => fetchResults(query)) // 异步获取结果  
    .listen(updateUI);                     // 更新界面  

通过声明式管道清晰表达业务规则避免回调嵌套逻辑层次分明


3.4、与Flutter生态深度整合

StreamFlutter响应式编程体系的核心基础设施:

  • 状态管理BLoCRiverpod等库依赖Stream实现状态变化通知。
  • UI更新StreamBuilder组件将数据流自动映射到界面重建。
  • 跨组件通信 :通过Stream实现松散耦合的组件间数据传递。

计数器应用

dart 复制代码
// BLoC类管理计数逻辑  
class CounterBloc {  
  final _counterController = StreamController<int>();  
  int _count = 0;  

  Stream<int> get counter => _counterController.stream;  

  void increment() {  
    _count++;  
    _counterController.sink.add(_count);  
  }  

  void dispose() => _controller.close();  
}  

// UI层通过StreamBuilder绑定数据  
StreamBuilder<int>(  
  stream: counterBloc.counter,  
  builder: (context, snapshot) {  
    return Text('Count: ${snapshot.data ?? 0}');  
  },  
)  

这种模式将业务逻辑UI彻底解耦,提升代码可测试性可维护性


3.5、处理复杂异步协作场景

Stream提供丰富的组合操作符,简化多数据流协作:

  • 并行处理Stream.asyncExpand同时处理多个异步任务
  • 数据合并StreamZip同步多个流的最新数据
  • 错误隔离handleError实现细粒度的异常捕获与恢复

表单多字段验证

dart 复制代码
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(); // 仅在验证状态变化时触发  

通过流合并自动跟踪多个输入字段状态,实时更新表单提交按钮的可用性。


3.6、核心价值体系

维度 传统方式缺陷 Stream解决方案
数据处理模式 一次性加载,内存压力大 逐块流式处理,内存占用恒定
代码可维护性 回调嵌套,逻辑分散 声明式管道,逻辑集中可追溯
实时响应能力 轮询开销大,响应延迟 订阅推送机制,即时触发
复杂场景支持 手动协调多任务,易出错 内置操作符处理合并、错误、流量控制

掌握Stream意味着获得:

  • 1、系统化的异步编程思维 :将数据流动抽象为可观测可控制的管道。
  • 2、高效的技术工具集 :通过操作符组合解决复杂异步问题。
  • 3、面向未来的架构能力 :深度融入Flutter响应式生态,构建可扩展应用。

Stream不是可选技能,而是处理异步编程的核心基础设施 。它如同编程世界的"中枢神经系统",让数据在应用中智能流动,驱动功能模块高效协作。

四、属性详解

4.1、Stream属性详解

属性 类型 作用描述 注意事项
isBroadcast(原生) bool 标识是否为广播流(允许多个监听者)。 单订阅流不可重复监听;广播流需通过asBroadcastStream()StreamController.broadcast()创建。
isEmpty(原生) Future<bool> 异步判断流是否为空(无任何数据事件)。 需等待流完成;调用后消费数据,后续无法监听。
first(扩展) Future<T> 获取流的第一个数据事件(流为空时抛出错误)。 若流未关闭且无数据会永久等待;需用awaitcatchError处理。
last(扩展) Future<T> 获取流的最后一个数据事件(流为空时抛出错误)。 必须等待流关闭;对无限流无效。
single(扩展) Future<T> 检查流是否仅有一个数据事件(流为空或有多个数据时抛出错误)。 必须等待流关闭;误用于多数据流会抛出错误。
length(扩展) Future<int> 计算流中数据事件的总数量。 会消费整个流的数据;对无限流导致永久阻塞。

关键说明

  • 原生属性 :直接定义在 Stream 类中(如 isBroadcastisEmpty)。

  • 扩展方法 :通过 dart:async 扩展机制实现,语法类似属性(如 stream.first),本质为异步操作

  • 通用规则 :除 isBroadcast 外,其他属性/方法均需 await;直接调用会消费流数据,导致流无法重复使用。

  • 开发建议

    • 优先使用 StreamBuilder 或显式 listen() 处理流数据。
    • 避免直接调用 first/last 等扩展方法,除非明确需要单次数据消费,建议通过 take(1)where 限制数据范围。
    • 对广播流需手动管理订阅的取消(subscription.cancel())。

常见问题(FAQ):

  • Q : 为什么 isEmpty 返回 Future<bool> 而不是 bool
    A: 流的异步特性决定了必须等待数据传递完成才能判断是否为空。

  • Q : isBroadcasttrue 时是否一定支持多监听?
    A : 是的,但需确保数据源支持多监听(如 StreamController.broadcast)。

  • Q : 如何避免 single 方法抛出错误?
    A : 使用 take(1) 限制数据量:

    dart 复制代码
    final result = await stream.take(1).single;  

4.2、StreamController属性详解

属性 类型 作用描述 注意事项
stream Stream<T> 控制器关联的输出流,供外部监听数据。 必须通过 listen() 订阅后才能接收数据。
sink StreamSink<T> 数据入口,用于添加数据(add())、错误(addError())或关闭流(close())。 调用 close() 后继续添加数据会抛出 StateError
isClosed bool 标识控制器是否已关闭(不再接收新数据)。 关闭后不可逆,需通过 controller.close() 终止。
isPaused bool 标识流是否被暂停(通过 pause() 方法)。 需手动调用 resume() 恢复数据流动。
hasListener bool 标识是否有活跃的监听者(通过 listen() 订阅且未取消)。 广播流中可能有多个监听者,但此属性仅表示至少存在一个。
done Future<void> 控制器关闭时完成的 Future(通过 close() 触发)。 用于等待流完全关闭(如资源释放后执行回调)。

关键说明

①、isPaused

  • 调用 pause() 会暂停数据发送,但不会缓存数据,恢复后仅发送后续数据。
  • 若需缓存暂停期间的数据,需配合 StreamController(sync: true) 或自定义缓冲逻辑。

②、done

dart 复制代码
 await controller.close();  
 await controller.done; // 确保流完全关闭  

③、hasListener

  • 单订阅流在监听后,hasListenertrue,取消订阅后为 false
  • 广播流中任意监听者存在时,hasListenertrue

④、sinkstream 的对称性

  • sink 是写入端,stream 是读取端,二者共同构成完整的流管道。

完整生命周期示例

dart 复制代码
final controller = StreamController<int>();  

// 监听数据流  
final subscription = controller.stream.listen(print);  

// 添加数据  
controller.sink.add(1);  
controller.sink.add(2);  

// 暂停流  
controller.sink.pause();  
print(controller.isPaused); // 输出:true  

// 恢复流  
controller.sink.resume();  

// 关闭流  
await controller.close();  
print(controller.isClosed); // 输出:true  

// 等待流完全关闭  
await controller.done;  

五、核心方法详解

5.1、工厂构造函数

①、.fromIterable(Iterable<T> elements)

作用 :从同步集合(如 ListSet)创建流,按顺序发射所有元素后关闭。
场景:将内存中的数据集转换为流式处理管道。

dart 复制代码
Stream<int> stream = Stream.fromIterable([1, 2, 3]);  
stream.listen(print); // 输出:1 → 2 → 3 → onDone  

注意事项

  • 数据立即发射,适用于已知数据集。
  • 若需异步生成数据,改用 async* 生成器。

②、.fromFuture(Future<T> future)

作用 :将单个 Future 转换为流,发射其结果(成功值或错误)后关闭。
场景 :将异步操作(如网络请求文件 I/O)整合到流中。

dart 复制代码
Future<int> fetchData() async => 42;  
Stream.fromFuture(fetchData()).listen(print); // 输出:42 → onDone  

注意事项

  • Future 失败,流会发射错误事件并关闭。

③、.fromFutures(Iterable<Future<T>> futures)

作用 :将多个 Future 转换为流,发射其结果(成功值或错误)后关闭。
场景 :将多个异步操作整合到流中。

dart 复制代码
Future<int> waitTask() async {
  await Future.delayed(Duration(seconds: 2));
  return 10;
}

Future<String> doneTask() async {
  await Future.delayed(Duration(seconds: 5));
  return 'Future complete';
}

void main() {
  final stream = Stream<Object>.fromFutures([doneTask(), waitTask()]);
  stream.listen(print, onDone: () => print('Done'), onError: print);
}

// 输出:
// 10(2秒后)
// Future complete(5秒后)
// Done

注意事项

  • 单订阅流 :不可多次调用 listen(),否则抛出 StateError
  • 错误处理 :任意 Future 抛出错误会触发流的 onError,但流继续处理其他 Future
  • 顺序问题 :结果按 Future 完成顺序发射,futures 的原始顺序

④、.periodic(Duration period, [T Function(int)? computation])

作用周期性生成事件流,每个周期触发一次。
场景 :定时任务(如轮询心跳检测)。

dart 复制代码
// 每秒发射一个递增整数  
Stream.periodic(Duration(seconds: 1), (count) => count)  
    .take(3)  
    .listen(print); // 输出:0 → 1 → 2 → onDone  

注意事项

  • computation 可为空,此时事件值为 null
  • 使用 take/takeWhile 限制事件数量,避免无限流。

⑤、Stream.value(T value)

作用 :创建单值流,立即发射数据后关闭。
场景 :快速包装常量简单值

dart 复制代码
Stream.value("Hello").listen(print); // 输出:Hello → onDone  

注意事项

  • 等效于 Stream.fromIterable([value]),但更简洁。

⑥、.error(Object error, [StackTrace? stackTrace])

作用 :创建错误流,立即发射错误后关闭。
场景:模拟错误场景或传递异常。

dart 复制代码
Stream.error(Exception("Timeout"))  
    .listen(null, onError: print); // 输出:Exception: Timeout → onDone  

注意事项

  • 必须处理 onError,否则错误会向上传播。

⑦、.empty()

作用 :创建空流,直接触发完成事件。
场景占位条件分支中无数据的场景。

dart 复制代码
Stream.empty().listen(  
  print,  
  onDone: () => print("Completed"),  
); // 输出:Completed  

⑧、.multi(void onListen(StreamController<T> controller))

作用手动控制流事件,通过回调暴露 StreamController
场景 :需要动态生成数据自定义复杂逻辑

dart 复制代码
Stream<int> countStream(int max) {  
  return Stream.multi((controller) {  
    for (int i = 1; i <= max; i++) {  
      controller.add(i);  
    }  
    controller.close();  
  });  
}  
countStream(3).listen(print); // 输出:1 → 2 → 3 → onDone  

注意事项

  • 必须手动关闭控制器,否则监听者会无限等待。
  • 适用于需要完全控制事件发射顺序和时机的场景。

⑨、.eventTransformed(Stream source, EventSink<T> transform(EventSink<T> sink))

作用 :通过自定义 EventSink 转换源流的事件。
场景 :实现底层流转换逻辑(如自定义协议解析)。

dart 复制代码
final source = Stream.fromIterable([1, 2, 3]);  
final transformed = Stream.eventTransformed(source, (sink) => MyCustomSink(sink));  

注意事项

  • 需实现 EventSink 接口,处理 addaddErrorclose 事件。
  • 通常用于框架开发,普通业务代码优先使用 transform + StreamTransformer

⑩、选择策略

场景 推荐方法 替代方案
同步数据集转流 fromIterable async* 生成器
单次异步操作转流 fromFuture Future.then + StreamController
多个 Future 处理 fromFutures fromIterable + asyncMap
定时/周期性事件 periodic Timer.periodic + StreamController
快速包装单值 value fromIterable([value])
错误模拟或传递 error throw + async*
动态生成数据流 multi 自定义 StreamTransformer

5.2、核心高频使用方法:第一梯队

几乎每个 Stream 使用场景都会用到的方法,覆盖 80% 的日常开发需求。

①、listen(void onData(T event)?, {onError, onDone, cancelOnError})

作用 :订阅流并处理数据错误完成事件,返回 StreamSubscription 对象。

dart 复制代码
final subscription = stream.listen(
  (data) => print(data),
  onError: (e) => print("Error: $e"),
  onDone: () => print("Done"),
);

注意事项

  • 必须手动取消订阅 :调用 subscription.cancel() 避免内存泄漏。
  • cancelOnError :默认为 false,若设为 true,第一个错误会取消订阅。

②、map<S>(S Function(T) convert)

作用:同步转换每个数据事件。

dart 复制代码
stream.map((num) => num * 2).listen(print); // 输入1 → 输出2  

注意事项

  • 转换函数应为纯函数(无副作用)。

③、where(bool Function(T) test)

作用:过滤不符合条件的数据事件。

dart 复制代码
stream.where((num) => num > 10).listen(print);  

注意事项

  • 过滤条件应避免复杂计算,防止阻塞事件循环。

④、asyncMap<S>(FutureOr<S> Function(T) convert)

作用 :异步转换每个数据事件(返回 Future)。

dart 复制代码
stream.asyncMap((id) => fetchUser(id)).listen(print);  

注意事项

  • 输出顺序与输入顺序一致,即使异步任务完成时间不同。

⑤、handleError(Function onError, {bool test(Object error)?})

作用:捕获并处理流中的错误事件。

dart 复制代码
stream.handleError(
  (e) => print("Handled: $e"),
  test: (e) => e is NetworkException,
);

注意事项

  • 必须重新抛出未处理的错误,否则会静默忽略。

⑥、take(int count)

作用 :仅取前 count 个数据后关闭流。

dart 复制代码
stream.take(3).listen(print); // 仅接收前3个数据  

⑦、skip(int count)

作用 :跳过前 count 个数据。

dart 复制代码
stream.skip(2).listen(print); // 跳过前2个数据  

5.3、控制流方法:第二梯队

用于复杂流控制或数据流整形,覆盖 15% 的中级场景。

①、expand<S>(Iterable<S> Function(T) convert)

作用 :将每个数据事件展开为多个事件(类似 flatMap)。

dart 复制代码
stream.expand((num) => [num, num * 10]).listen(print); // 输入1 → 输出1,10  

②、takeWhile(bool Function(T) test)

作用 :取数据直到条件为 false,之后关闭流。

dart 复制代码
stream.takeWhile((num) => num < 5).listen(print);  

③、skipWhile(bool Function(T) test)

作用 :跳过数据直到条件为 false,之后保留剩余数据。

dart 复制代码
stream.skipWhile((num) => num < 3).listen(print);  

④、distinct([bool Function(T, T)? equals])

作用:跳过连续重复的数据事件。

dart 复制代码
stream.distinct().listen(print); // 输入1,1,2 → 输出1,2  

5.4、高级操作与资源管理:第三梯队

用于底层流操作或资源控制,覆盖 5% 的高级场景。

①、transform<S>(StreamTransformer<T, S> transformer)

作用:应用自定义转换器(如编解码、协议解析)。

scss 复制代码
dart
stream.transform(utf8.decoder).listen(print);  

注意事项

  • 需实现 StreamTransformer 接口。

②、pipe(StreamConsumer<T> consumer)**

作用 :将流数据直接传输到 StreamConsumer(如文件写入)。

dart 复制代码
stream.pipe(File('output.txt').openWrite());

③、drain<T>([T? futureValue])

作用:消费流中所有剩余数据但不处理,用于资源清理。

dart 复制代码
await stream.drain(); // 确保流完全消费  

④、cast<S>()

作用:将流的数据类型强制转换为指定类型。

dart 复制代码
Stream<num> numbers = Stream<int>.fromIterable([1, 2, 3]).cast<num>();

⑤、asBroadcastStream()

作用:将单订阅流转换为广播流。

dart 复制代码
final broadcastStream = stream.asBroadcastStream();

5.5、边缘或聚合操作:第四梯队

用于特定场景的聚合操作或边缘需求。

①、contains(Object? value)

作用 :检查流是否包含指定值,返回 Future<bool>

dart 复制代码
final exists = await stream.contains(42);

②、forEach(void Function(T) action)

作用 :对每个数据执行操作,返回 Future<void>

dart 复制代码
await stream.forEach(print);  

③、reduce(T Function(T, T) combine)

作用:聚合所有数据为单个结果(需流非空)。

dart 复制代码
final sum = await stream.reduce((a, b) => a + b);  

④、join([String separator = ""])

作用:将流中的数据拼接为字符串。

dart 复制代码
final result = await stream.join(",");  

⑤、every(bool Function(T) test)

作用 :检查所有数据是否满足条件,返回 Future<bool>

dart 复制代码
final allValid = await stream.every((num) => num > 0);  

5.6、选择策略

梯队 方法 核心用途 使用频次
第一梯队 listen, map, where 基础数据订阅、转换和过滤 极高
第二梯队 expand, takeWhile 流数据整形与控制
第三梯队 transform, asBroadcast 底层流操作与资源管理
第四梯队 contains, reduce 聚合操作与边缘需求 极低

六、基本用法

Stream 的基本使用流程可分为四步:创建流 → 监听流 → 操作流 → 关闭流

6.1、创建流

①、工厂构造函数(推荐)

arduino 复制代码
// 1、从集合创建 同步数据流
Stream<int> stream = Stream.fromIterable([1, 2, 3]);

// 2、从异步任务创建 异步数据流
Stream<int> stream =
    Stream.fromFuture(Future.delayed(Duration(seconds: 1), () => 42));

// 3、创建周期性数据流 每秒发射递增整数
Stream<int> stream =
    Stream.periodic(Duration(seconds: 1), (count) => count);

②、使用 StreamController动态控制流

dart 复制代码
final controller = StreamController<int>();  

// 添加数据  
controller.sink.add(1);  
controller.sink.addError(Exception("Error"));  
controller.sink.close();  

// 获取输出流  
Stream<int> outputStream = controller.stream;  

6.2、监听流

①、基础监听

dart 复制代码
StreamSubscription<int> subscription = stream.listen(
  (data) => print("数据: $data"),  // 处理数据
  onError: (error) => print("错误: $error"),  // 处理错误
  onDone: () => print("流已关闭"),  // 处理完成
  cancelOnError: true,  // 第一个错误时自动取消订阅
);  

②、订阅管理

dart 复制代码
// 取消订阅
subscription.cancel();  // 释放资源,防止内存泄漏 

//暂停/恢复
subscription.pause();   // 暂停接收数据  
subscription.resume();  // 恢复接收数据 

6.3、操作流

①、转换数据

dart 复制代码
// 同步转换(map)
stream.map((num) => num * 2).listen(print); // 输入1 → 输出2  

// 异步转换(asyncMap)
stream.asyncMap((id) => fetchUser(id)).listen(print); // 异步请求用户数据 

②、过滤数据

dart 复制代码
// 条件过滤(where)
stream.where((num) => num > 10).listen(print); // 只保留大于10的数据  

// 数量限制(take/skip)
stream.take(3).listen(print);  // 仅取前3个数据  
stream.skip(2).listen(print); // 跳过前2个数据  

③、错误处理

dart 复制代码
// 全局捕获错误(`handleError`)
stream.handleError(
    (e) => print("捕获错误: $e"), 
    test: (e) => e is NetworkException  // 只处理特定错误
).listen(print);  

6.4、关闭流与资源释放

①、关闭 StreamController

dart 复制代码
controller.close(); // 关闭控制器,触发onDone事件  

②、使用 await for 处理流

dart 复制代码
try {
  await for (final data in stream) {
    print(data);
  }
} catch (e) {
  print("捕获错误: $e");
}  

③、清理资源

dart 复制代码
// 取消所有订阅
subscription.cancel();  

// 释放控制器
if (!controller.isClosed) {  
    await controller.close();  
}  

6.5、小试牛刀:实时搜索功能

dart 复制代码
// 1. 创建输入流(如搜索框文本变化)  
final searchController = StreamController<String>();  

// 2. 操作流:过滤空值、请求API  
searchController.stream  
    .where((query) => query.isNotEmpty)     // 过滤空字符串  
    .asyncMap((query) => fetchSearchResults(query)) // 异步搜索  
    .listen(updateUI); // 更新界面  

// 3. 模拟用户输入  
searchController.sink.add("Dart");  
searchController.sink.add("Flutter");  

// 4. 关闭资源  
await searchController.close();  

七、设计哲学

7.1、分层架构设计

Stream 的运行机制可分为三个层次

①、生产者层

  • 数据源 :产生异步事件的源头,包括:
    • 外部输入用户交互网络请求文件 I/O
    • 生成器 :通过async*函数按需生成数据。
    • 控制器StreamController手动管理数据推送。

②、处理管道

  • 操作符链 :通过mapwheretransform等方法对数据流进行转换过滤聚合
  • 内存管理:逐条处理数据,避免一次性加载全部数据到内存。

③、消费者层

  • 订阅机制 :通过listen()await for监听数据流。
  • 资源释放 :通过cancel()终止监听,或close()关闭数据源。

核心代码结构解析

dart 复制代码
// 1. 生产者层:创建数据流  
Stream<int> generateData() async* {  
  for (int i = 1; i <= 5; i++) {  
    await Future.delayed(Duration(seconds: 1));  
    yield i; // 每秒发送一个数字  
  }  
}  

// 2. 处理管道:转换与过滤  
final processedStream = generateData()  
    .map((num) => num * 2)      // 数值翻倍  
    .where((num) => num > 3);   // 过滤小于等于3的值  

// 3. 消费者层:订阅数据流  
final subscription = processedStream.listen(  
  (data) => print('接收数据: $data'),  
  onError: (err) => print('错误: $err'),  
  onDone: () => print('流已关闭'),  
);  

// 4. 资源释放(可选)  
Future.delayed(Duration(seconds: 3), () => subscription.cancel());  

7.2、核心行为模式

①、订阅驱动

  • 数据仅在存在活跃订阅者时流动,无监听者时数据可能被丢弃。
  • 示例:StreamController的数据在无监听者时不会被缓存

②、背压控制

  • 通过pause/resume动态调节数据流速,防止消费者处理不过来导致内存堆积

③、错误传播

  • 错误事件沿操作符链向上传递,直到被handleError捕获或导致程序崩溃

7.3、与 Future 的本质区别

维度 Stream Future
数据量 处理多个异步事件 处理单个异步结果
生命周期 持续存在,直到主动关闭 一次性完成(成功或失败)
监听机制 支持多次监听(广播流)或单次监听 仅单次完成,无法重复监听
典型场景 实时聊天、文件流式传输、用户事件监听 单次网络请求、数据库查询

八、总结

掌握Stream的秘诀在于构建数据管道思维

  • 1、理解本质 :把流视为可编程的数据管道,关注数据入口→加工环节→出口消费
  • 2、层次递进:从基础监听→操作符链式调用→多流协作,逐级解锁高阶用法 。
  • 3、实战驱动 :将聊天室、分页加载等场景抽象为流模型,用listen+transform+combine组合拳解决。

优秀的Flutter开发者不仅是界面工程师 ,更是数据流动的架构师 。当你能像设计水利工程一样设计数据管道时,便是真正系统化掌握了Stream的精髓。

欢迎一键四连关注 + 点赞 + 收藏 + 评论

相关推荐
小墙程序员1 小时前
Flutter 教程(四)包管理
flutter
威哥爱编程1 小时前
谷歌Android闭源与鸿蒙崛起:一场关于技术主权的生态博弈
android·harmonyos
小鱼人爱编程2 小时前
Flutter 打包APK的几种方式
android·前端·后端
sunly_3 小时前
Flutter:切换账号功能记录
android·java·flutter
getapi3 小时前
Flutter和React Native在开发app中,哪个对java开发工程师更适合
java·flutter·react native
casual_clover4 小时前
Android 设备实现 adb connect 连接的步骤
android·adb
恋猫de小郭4 小时前
Android 确定废弃「屏幕方向锁定」等 API ,如何让 App 适配大屏和 PC/XR 等场景
android·前端·flutter
神仙别闹5 小时前
基于Java(SSM)+Mysql实现移动大厅业务办理(增删改查)
android·java·mysql
木子庆五7 小时前
Android设计模式之模板方法模式
android·设计模式·模板方法模式
孤舟簔笠翁9 小时前
【Audio开发一】android音频问题排查指南
android·音视频