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

351 阅读4分钟

image.png

前言

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)。

    • 包裹产生规则

      // 示例:模拟快递包裹生成(每秒产生一个包裹)  
      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通过逐块处理数据解决这些问题:

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

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


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

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

搜索框优化示例

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

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


3.4、与Flutter生态深度整合

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

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

计数器应用

// 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实现细粒度的异常捕获与恢复

表单多字段验证

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: 流的异步特性决定了必须等待数据传递完成才能判断是否为空。

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

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

    final result = await stream.take(1).single;  
    

 4.2、StreamController属性详解

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

关键说明

①、isPaused

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

②、done

 await controller.close();  
 await controller.done; // 确保流完全关闭  

③、hasListener

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

④、sink 与 stream 的对称性

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

完整生命周期示例

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)创建流,按顺序发射所有元素后关闭。
场景:将内存中的数据集转换为流式处理管道。

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

注意事项

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

②、.fromFuture(Future<T> future)

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

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

注意事项

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

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

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

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])

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

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

注意事项

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

⑤、Stream.value(T value)

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

Stream.value("Hello").listen(print); // 输出:Hello → onDone  

注意事项

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

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

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

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

注意事项

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

⑦、.empty()

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

Stream.empty().listen(  
  print,  
  onDone: () => print("Completed"),  
); // 输出:Completed  

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

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

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 转换源流的事件。
场景:实现底层流转换逻辑(如自定义协议解析)。

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

注意事项

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

⑩、选择策略

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

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

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

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

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

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)

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

stream.map((num) => num * 2).listen(print); // 输入1 → 输出2  

注意事项

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

③、where(bool Function(T) test)

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

stream.where((num) => num > 10).listen(print);  

注意事项

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

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

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

stream.asyncMap((id) => fetchUser(id)).listen(print);  

注意事项

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

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

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

stream.handleError(
  (e) => print("Handled: $e"),
  test: (e) => e is NetworkException,
);

注意事项

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

⑥、take(int count)

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

stream.take(3).listen(print); // 仅接收前3个数据  

⑦、skip(int count)

作用:跳过前 count 个数据。

stream.skip(2).listen(print); // 跳过前2个数据  

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

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

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

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

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

②、takeWhile(bool Function(T) test)

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

stream.takeWhile((num) => num < 5).listen(print);  

③、skipWhile(bool Function(T) test)

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

stream.skipWhile((num) => num < 3).listen(print);  

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

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

stream.distinct().listen(print); // 输入1,1,2 → 输出1,2  

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

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

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

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

stream.transform(utf8.decoder).listen(print); 

注意事项

  • 需实现 StreamTransformer 接口。

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

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

stream.pipe(File('output.txt').openWrite());

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

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

await stream.drain(); // 确保流完全消费  

④、cast<S>()

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

Stream<num> numbers = Stream<int>.fromIterable([1, 2, 3]).cast<num>();

⑤、asBroadcastStream()

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

final broadcastStream = stream.asBroadcastStream();

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

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

①、contains(Object? value)

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

final exists = await stream.contains(42);

②、forEach(void Function(T) action)

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

await stream.forEach(print);  

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

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

final sum = await stream.reduce((a, b) => a + b);  

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

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

final result = await stream.join(",");  

⑤、every(bool Function(T) test)

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

final allValid = await stream.every((num) => num > 0);  

5.6、选择策略

梯队方法核心用途使用频次
第一梯队listenmapwhere基础数据订阅、转换和过滤极高
第二梯队expandtakeWhile流数据整形与控制
第三梯队transformasBroadcast底层流操作与资源管理
第四梯队containsreduce聚合操作与边缘需求极低

六、基本用法

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

6.1、创建流

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

// 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动态控制流

final controller = StreamController<int>();  

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

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

③、使用 async* 生成器

Stream<String> generatePackages() async* {
  int count = 1;
  while (true) {
    await Future.delayed(Duration(seconds: 1));
    yield "包裹-$count"; // 生成包裹,如"包裹-1", "包裹-2"
    count++;
  }
}

6.2、监听流

①、基础监听

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

②、订阅管理

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

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

6.3、操作流

①、转换数据

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

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

②、过滤数据

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

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

③、错误处理

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

6.4、关闭流与资源释放

①、关闭 StreamController

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

②、使用 await for 处理流

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

③、清理资源

// 取消所有订阅
subscription.cancel();  

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

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

// 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()关闭数据源。

核心代码结构解析

// 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 的本质区别

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

八、进阶应用

8.1、时股票价格监控系统

需求描述: 金融类应用需要实时显示股票价格变动,要求数据每秒更新且UI无卡顿。当价格波动超过阈值时触发警告。

import 'dart:async';
import 'dart:math';
import 'package:flutter/material.dart';

class StockData {
  final String symbol;
  final double price;
  final DateTime timestamp;

  StockData(this.symbol, this.price, this.timestamp);
}

/// Stream 控制器
class StockStreamProvider {
  final _controller = StreamController<StockData>.broadcast();

  Stream<StockData> get stream => _controller.stream;

  void simulateData() {
    Timer.periodic(Duration(seconds: 1), (_) {
      final price = 100 + Random().nextDouble() * 10;
      _controller.add(StockData('AAPL', price, DateTime.now()));
    });
  }

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

/// UI 展示
class StockMonitorScreen extends StatelessWidget {
  const StockMonitorScreen({super.key});


  @override
  Widget build(BuildContext context) {
    final stockProvider = StockStreamProvider();
    stockProvider.simulateData();
    return Scaffold(
      appBar: AppBar(
        backgroundColor: Colors.blueAccent,
        title: Text("Stream Demo"),
      ),
      body: StreamBuilder<StockData>(
        stream: stockProvider.stream,
        builder: (context, snapshot) {
          if (!snapshot.hasData) return CircularProgressIndicator();
          final data = snapshot.data!;
          return Column(
            children: [
              Text('${data.symbol}: $${data.price.toStringAsFixed(2)}'),
              if (data.price > 108)
                Text('预警:价格过高!', style: TextStyle(color: Colors.red))
            ],
          );
        },
      ),
    );
  }
}

技术要点

  • 使用StreamController.broadcast()实现多订阅。
  • 通过Timer.periodic模拟实时数据源。
  • StreamBuilder自动处理数据更新和UI重建。

九、总结

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

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

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

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