Dart - 全面认识Stream

9 阅读18分钟

第一章:Flutter 进阶——为什么你需要 Stream?从 Future 到流的思维跃迁

在 Flutter 和 Dart 的异步编程世界里,大多数开发者都是从 Future 开始入门的。我们习惯了 await 一个网络请求,然后等待结果返回。

但是,当你试图实现一个“倒计时”、“文件下载进度条”或者“实时聊天室”时,你会发现 Future 变得力不从心。这时候,你就需要升级你的武器库,引入一个更强大的概念 —— Stream (流)

本章我们将不再仅仅罗列 API,而是从内存和执行原理的角度,解剖 Stream 到底是什么,以及它为什么被称为“异步数据的生命之河”。

一、 Future 的“一锤子买卖”

先来看一段我们最熟悉的 Future 代码:

Future<String> fetchUser() async {
  await Future.delayed(Duration(seconds: 2));
  return "Jason"; // 任务结束,返回结果
}

Future 的设计哲学非常简单:一次请求,一次响应。

它就像是网购。你下个单(调用函数),过几天快递员给你一个包裹(返回值)。交易完成后,你和快递员的关系就结束了。

在底层内存模型中,当你执行 return "Jason" 的那一瞬间,发生了两件事:

  1. 结果交付:数据被发送给了等待者。
  2. 现场销毁fetchUser 函数的栈帧 (Stack Frame) 被弹出、销毁。这个函数“死”了,它的生命周期彻底结束。

痛点来了: 如果你想要的是“连续的数据”呢? 比如,你不仅想知道“文件下载完了没有”,还想知道“现在下载了百分之几”。

  • 如果你用 Future,你只能得到一张下载完成时的截图
  • 但你真正想要的是一段录像

这时候,我们需要一个能“活着”并在时间轴上源源不断吐出数据的机制。

二、 Stream:时间轴上的传送带

如果说 Future 是静态的,那么 Stream 就是动态的线

你可以把 Stream 想象成回转寿司店里的一条自动传送带

在这个模型里,有三个核心角色:

  1. Sink (入口/厨师):这是生产端。厨师(生成器)把一盘盘寿司(数据)按顺序放上传送带。
  2. Stream (管道):这是传送带本身。它负责搬运数据,它不关心谁吃,只负责转动。
  3. Listener (出口/食客):这是消费端。你坐在传送带末端,监听 (Listen) 着它。来一盘,你吃一盘。

Future 不同,Stream 是一种 异步的 Iterable (Asynchronous Iterable)。它代表的不是“一个值”,而是“可能随时间推移而到达的一系列值”。

三、 语法上的“基因突变”:async* 与 yield

为了支持这种“源源不断”的特性,Dart 在语法层面做了一个极具深意的设计。

我们要重点关注两个关键字:async*yield

1. 那个神秘的星号 (*)

你可能注意到了,Stream 函数的定义后面必须带一个 *

// Future: 单数
Future<int> getScore() async { ... }

// Stream: 复数(生成器)
Stream<int> getScores() async* { ... }

这个星号 * 代表 Generator (生成器)。在计算机科学中,它暗示着“多”和“生产能力”。它告诉编译器:“嗨,这个函数有点特殊,它不会跑一遍就死掉,它是一个状态机。”

2. yield vs return:暂停与销毁

这是理解 Stream 底层原理最关键的一步。请看这段代码:

Stream<int> countDown() async* {
  for (int i = 5; i > 0; i--) {
    await Future.delayed(Duration(seconds: 1));
    yield i; // <--- 关键看这里!
  }
}

  • return (辞职): 当普通函数执行 return 时,它是彻底退出。它的栈帧被销毁,局部变量全部清空。下次再调用,一切从头开始。
  • yield (停薪留职/暂停): 当生成器函数执行 yield i 时,它做的是 “交出数据,原地暂停”
  • 交出数据:把 i 扔进事件循环,发给监听者。
  • 保留现场关键点! 此时函数的栈帧并没有被销毁!当前的局部变量 i 的值、代码执行到了第几行,通通被“冻结”在内存里。
  • 恢复执行:当函数再次被唤醒时,它会从 yield下一行继续执行,仿佛从未中断过。

正是因为有了 yield 这种**“保留状态”**的能力,Stream 才能做到记住了循环到了哪里,从而源源不断地产生数据。

四、 为什么我们需要 Stream?

既然 Future 简单好用,为什么还要折腾 Stream

1. 解决“过程”问题 现实世界的交互往往是连续的。

  • 倒计时:5, 4, 3, 2, 1...
  • 搜索联想:你输一个字母,推荐列表变一次。
  • WebSocket:服务器随时可能推一条新消息过来。 这些场景,用 Future 这种“一次性承诺”是无法优雅实现的,必须用 Stream

2. 变“主动轮询”为“被动响应”

  • 传统方式 (Pull):你不停地问服务器“好了没?好了没?”(轮询),浪费资源。
  • Stream 方式 (Push):你注册一个监听器 listen(),然后去干别的事。一旦有数据,Stream 会主动推给你。这也就是现在流行的 响应式编程 (Reactive Programming) 的核心思想。

小结

特性Future (未来)Stream (流)
数据量单个值 (Single)多个值 (Multiple)
生命周期一次性 (One-shot)持续的时间轴 (Continuous)
结束动作Return (销毁)Stream Done (关闭)
核心机制栈帧销毁栈帧暂停 (yield)
生活类比拍一张照片录一段视频

理解了 Stream 的传送带模型yield 的暂停机制,你就已经迈过了异步编程最难的一道坎。

但是,现在的传送带还很简陋。如果我想让多个人同时看一条传送带(多订阅)?或者我想在传送带中间加一个滤网,只过滤出我想要的寿司(操作符)?

下一章,我们将深入探讨 Stream 的两种形态:单订阅 (Single-subscription)广播 (Broadcast)


第二章:Flutter 进阶——Stream 的两种形态与掌控权

在上一章中,我们用 async* 函数轻松创建了一条传送带。 但是,当你试图在代码中对同一个 Stream 调用两次 listen 时,程序会毫不留情地抛出一个异常: Bad state: Stream has already been listened to.

这并不是 Bug,这是 Dart Stream 设计哲学的核心:根据消费场景的不同,Stream 分为两种截然不同的形态。

本章我们将深入探讨 单订阅 (Single-subscription)广播 (Broadcast) 的区别,并解锁 Stream 的手动挡模式 —— StreamController

一、 私密对话 vs 公共广播

在内存世界里,数据的流动方式决定了 Stream 的类型。

1. 单订阅 Stream (Single-subscription) —— “我的汉堡”

这是 Stream 的默认形态。当你使用 async* 或者 File.openRead() 创建流时,它就是单订阅的。

  • 特点一对一。这条传送带是为你专门铺设的。
  • 形象比喻“在餐厅点餐”。 厨师为你做了一份炒饭。这份炒饭(数据)只能被你一个人吃(消费)。如果你的朋友也想吃,他必须重新下一单(创建一个新的 Stream ),厨师会重新做一份。
  • 底层逻辑: 数据是为了保证完整性顺序性。比如读取文件,你绝不希望两个人在同时读一个文件流,导致你读一半,他读一半,数据全乱套了。
  • 致命限制只能监听一次! 即使第一个监听者取消了订阅 (cancel),这条 Stream 也废了,不能再被监听。

2. 广播 Stream (Broadcast) —— “村口大喇叭”

这是 Stream 的另一种形态。通常用于事件总线、鼠标点击、系统通知等场景。

  • 特点一对多
  • 形象比喻“听收音机”。 电台(数据源)在不停地播放。你听,或者隔壁老王听,甚至一百个人同时听,互不影响。
  • 关键差异
  • 过时不候:广播流通常是 "Hot" (热) 的。如果你 10:00 打开收音机,你听不到 9:50 播放的新闻。数据发出去没人听,就直接丢弃了。
  • 随时监听:你可以随时加入,也可以随时退出,支持多个监听者同时存在。

3. 代码实战:如何转换?

如果我非要让那盘“炒饭”大家一起吃怎么办?Dart 提供了 asBroadcastStream() 方法。

// 1. 创建一个普通的单订阅流
Stream<int> stream = getScoreStream();

// 2. 强行变成广播流
Stream<int> broadcastStream = stream.asBroadcastStream();

// 3. 现在可以多次监听了
broadcastStream.listen((v) => print("老王听到了: $v"));
broadcastStream.listen((v) => print("小李听到了: $v"));


二、 手动挡:StreamController

到目前为止,我们都是通过 async* 函数来**“自动”**生成 Stream。这种方式很简单,但它是被动的——必须等到函数里的 yield 执行时才有数据。

如果我们想在一个按钮点击事件里发送数据?或者在网络请求回调里发送数据? 这时候,我们需要 StreamController (流控制器)

如果说 async* 是设定好程序的自动流水线,那 StreamController 就是一个万能遥控器

1. 结构解剖

StreamController 把 Stream 的结构拆解得清清楚楚:

  • 入口 (Sink):你可以随时随地调用 sink.add(data) 往里面扔数据。
  • 出口 (Stream):就是我们熟悉的那个 Stream,给别人去 listen 的。
  • 控制器 (Controller):管理开关、暂停、以及流的状态。

2. 极简代码示范

import 'dart:async';

void main() {
  // 1. 创建控制器 (买了一个遥控器)
  // 如果想做广播流,就用 StreamController.broadcast();
  final controller = StreamController<String>();

  // 2. 拿到出口 (给别人听的)
  controller.stream.listen(
    (data) => print("收到推流: $data"),
    onError: (err) => print("发生错误: $err"),
    onDone: () => print("直播结束"),
  );

  // 3. 拿到入口 (自己在任意地方控制)
  print("准备发射数据...");
  controller.sink.add("第一条消息"); // 像不像 EventBus?
  controller.sink.add("第二条消息");
  
  // 4. 模拟发生错误
  controller.addError("信号丢失!");

  // 5. 关流 (非常重要!!!)
  // 不关流会导致内存泄漏,因为监听者会一直干等着
  controller.close();
}

3. 为什么它在 Flutter 中如此重要?

几乎所有 Flutter 的状态管理库(BLoC, Provider, Riverpod 等)的底层,或多或少都用到了 StreamController 的思想。

  • UI 层:只管 add 事件(比如点击按钮)。
  • 逻辑层:通过 Controller 处理业务。
  • UI 层StreamBuilder 监听 Controller.stream 并刷新界面。

这就是 “输入与输出分离” 的架构雏形。


三、 避坑指南:内存泄漏的隐患

在使用 StreamController 时,有一个新手最容易犯的错误:忘了关流 (Close)

  • 原理StreamController 在底层会持有监听者的引用。如果你的页面销毁了,但 Controller 没关闭,这个 Stream 依然认为“有人在听”,它不会释放资源,导致 内存泄漏 (Memory Leak)
  • 铁律:在 Flutter 的 dispose() 方法中,一定要调用 controller.close()

小结

这一章我们完成了从“使用者”到“掌控者”的转变:

  1. 分清形态
  • 单订阅(默认):数据完整,一对一,错过即毁。
  • 广播(Broadcast):实时性强,一对多,过时不候。
  1. 掌握控制
  • 使用 StreamController 可以让我们在代码的任何地方主动地“推”数据,它是连接命令式代码(普通函数)和响应式代码(Stream)的桥梁。

了解了形态和控制,下一章我们将进入 Stream 最强大的领域 —— 数学般的魔法。 我们将探索如何像操作数组一样操作时间流:mapwheredebounce(防抖)以及 distinct。这些操作符将彻底改变你写业务逻辑的方式。

第三章:流上建造流水线

在上一章,我们学会了用 StreamController 制造传送带。但在真实开发中,原始数据往往是“脏”的或者“不符合 UI 胃口”的。

  • 后端:推给你一堆 JSON 字符串。
  • UI层:想要的是一个转换好的 User 对象。
  • 用户:手抖,一秒钟点了 5 次按钮。
  • 逻辑层:只希望处理最后一次点击。

如果把这些逻辑都写在 listen 的回调里,代码会变成一坨乱麻。 Dart Stream 赋予了我们一种能力:在数据到达监听者之前,先在传送带上架设一排“机器手臂”,对数据进行全自动加工。

这就是 操作符 (Operators)

一、 熟悉的配方:从 List 到 Stream

Dart 最优雅的设计之一,就是它让操作 Stream (时间流) 就像操作 List (静态数组) 一样简单。

如果你会用 List 的方法,你已经学会了 90% 的 Stream 操作。

1. 过滤与转换 (Where & Map)

想象传送带上流过来的是一堆数字 1, 2, 3, 4, 5...

  • 需求:我只想要偶数,而且要把它放大 10 倍。
Stream<int> rawStream = Stream.fromIterable([1, 2, 3, 4, 5]);

rawStream
    .where((event) => event % 2 == 0) // 机器手臂1:过滤。只放行偶数。
    .map((event) => event * 10)       // 机器手臂2:加工。变成原来的10倍。
    .listen((data) {
      print(data); // 输出:20, 40
    });

底层原理: 每个操作符(.where, .map)本质上都返回了一个新的 Stream。 这就像接水管一样,我们把一节节短管子(操作符)拧在一起,构成了一条长长的处理管道。原始数据从一头进,经过层层净化,最后流出来的就是我们想要的纯净水。

二、 解决现实痛点:那些 Stream 独有的神技

除了通用的 map/where,Stream 还有一些专门处理“时间轴”问题的神技。

1. 去重神技:distinct

  • 场景:你要实现一个搜索框。用户想搜 "Flutter",但他输入 "F", "Fl", "Flu"...
  • 痛点:如果用户输入了 "Flu",停了一下,删掉 "u",又输了一次 "u"。输入内容还是 "Flu"。如果不处理,你会发两次完全一样的网络请求。
  • 解法
inputStream
    .distinct() // 只有当新数据和上一次数据不一样时,才放行
    .listen((text) => search(text));

它就像一个极其严格的质检员,拿着上一个通过的产品做对比,一样的直接扔掉。

2. 扁平化神技:expandasyncExpand

这是一个高级但必用的操作符。

  • 场景:Stream 里流过来的是“文件夹”,但监听者想要的是“文件”。 即:Stream 发出的每个数据,本身又包含了一组数据(Stream of List)。
  • 解法expand 会把“流过来的每一个元素”炸开,变成一堆元素,然后重新铺平在传送带上。
// 假设流过来的是:[1, 2], [3, 4]
stream
  .expand((element) => element) 
  .listen(print); 
// 输出:1, 2, 3, 4 (变成了扁平的流)

三、 终极武器:StreamTransformer

有时候,官方提供的 mapwhere 不够用了。 比如,Socket 连接发过来的是字节流 (List<int>),但你想按**“换行符”切分成一行行的文本流 (String)**。

这时候,你需要自定义一个“变压器” —— StreamTransformer

它是 stream.transform() 方法的参数。Dart 官方贴心地在 dart:convert 库里内置了一些最常用的变压器:

import 'dart:convert';
import 'dart:io';

void readFile() {
  File('log.txt')
    .openRead() // 原始流:一堆二进制字节
    .transform(utf8.decoder) // 变压器1:字节 -> 字符串
    .transform(const LineSplitter()) // 变压器2:一整块字符串 -> 按换行符切开的一行行字符串
    .listen((line) {
      print("读取到一行日志: $line");
    });
}

底层逻辑transform 是将流的控制权完全交给你。你可以控制输入什么,缓存多少,什么时候输出,甚至可以把一个数据变成两个,或者把两个数据合并成一个。

四、 降维打击:RxDart 的防抖与节流

讲到 Stream 操作符,如果不提 RxDart,那就是耍流氓。 虽然 Dart 原生库很强,但在处理复杂的交互事件时,RxDart 提供了“外挂”级别的操作符。

Flutter 面试必问的两大杀手锏:

  1. 防抖 (Debounce)
  • 比喻:电梯关门。如果一直有人进电梯(事件一直来),电梯门就一直不关。只有当大家都不动了(间隔超过一定时间),电梯门才会关上(执行逻辑)。
  • 用途:搜索框联想。打字停顿 500ms 后再请求 API。
  1. 节流 (Throttle)
  • 比喻:机关枪射速限制。不管你扣扳机的手速有多快,子弹最快只能每秒发 10 发。
  • 用途:防止按钮连点。

(注:RxDart 本质上就是把 StreamTransformer 封装好了给你用。)

小结

这一章我们把 Stream 从“传输工具”升级成了“处理工具”。

  1. 管道思维:用 mapwhere 像搭积木一样处理数据。
  2. 独有技能:用 distinct 过滤重复信号。
  3. 高级定制:用 transform 处理复杂的数据转换(如二进制转文本)。

现在,我们有了数据源(Controller),有了处理逻辑(Operators),有了监听者(Listen)。 但是,在 Flutter 的 UI 代码里写 listensetState 依然很痛苦,很容易忘掉 cancel 导致内存泄漏。

有没有一种 Widget,能直接把 Stream 插上去,它自己就会根据数据变来变去,还自动管理内存?

下一章,我们将介绍 Flutter 官方提供的终极组件 —— StreamBuilder,它是连接逻辑层与 UI 层的跨海大桥。


第四章:Flutter 实战——告别 setState,拥抱 StreamBuilder

在前面的章节中,我们在纯 Dart 环境下把 Stream 玩出了花。但当我们回到 Flutter 的 Widget 世界时,会遇到一个尴尬的现实。

痛点:手动管理的“地狱”

如果你不用专门的工具,想在界面上显示一个 Stream 的数据,你需要写大量的模版代码:

  1. 必须用 StatefulWidget
  2. initState 里手动 listen
  3. 在回调里手动 setState 触发刷新。
  4. 最要命的:必须在 dispose 里手动 subscription.cancel()。哪怕忘写一次,你的 App 就会在后台默默发生内存泄漏,直到崩溃。

为了把开发者从这种重复劳动中解救出来,Flutter 提供了一个终极组件 —— StreamBuilder

一、 什么是 StreamBuilder?

StreamBuilder 是一个 Widget,但它不画任何东西。它的唯一工作就是 “自动帮你看传送带”

  • 自动化:它负责 listen,它负责 setState,它负责 dispose。你完全不用管。
  • 响应式:传送带上每过来一个新数据,它就自动调用一次 builder 方法,重新画一遍子组件。

二、 代码实战:一个最简单的电子表

我们来做一个每秒更新时间的电子表。

1. 准备 Stream(数据源)

Stream<String> getTimerStream() async* {
  while (true) {
    await Future.delayed(Duration(seconds: 1));
    yield DateTime.now().toString().substring(11, 19); // 返回 "12:00:01"
  }
}

2. 使用 StreamBuilder(UI 构建)

class MyClock extends StatelessWidget { // 注意:可以用 StatelessWidget 了!
  final Stream<String> _timerStream = getTimerStream();

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: StreamBuilder<String>(
          stream: _timerStream, // 1. 插上网线
          builder: (context, snapshot) { // 2. 根据快照画图
            // snapshot 包含了当前时刻 Stream 的所有信息
            if (snapshot.connectionState == ConnectionState.waiting) {
              return CircularProgressIndicator(); // 还没数据时显示转圈
            }
            
            if (snapshot.hasError) {
              return Text('出错了: ${snapshot.error}');
            }

            // 有数据了!
            return Text(
              snapshot.data ?? '无数据',
              style: TextStyle(fontSize: 30),
            );
          },
        ),
      ),
    );
  }
}

看,我们甚至不需要 StatefulWidget!所有的状态变化都封装在了 StreamBuilder 内部。

三、 解剖核心:AsyncSnapshot (快照)

builder 回调函数里那个 snapshot 参数,是理解 StreamBuilder 的关键。

你可以把它想象成 “Stream 在这一瞬间的体检报告”。它包含三个核心指标:

  1. ConnectionState (连接状态)
  • none: 没插网线(stream 为 null)。
  • waiting: 插了网线,但第一个数据还没来(通常显示 Loading)。
  • active: 数据正在源源不断地来(最主要的状态)。
  • done: 传送带停了(Stream 关闭)。
  1. data (数据)
  • 如果不为 null,说明这就是最新拿到的数据。
  1. error (错误)
  • 如果不为 null,说明刚才流里传来了一个错误事件。

最佳实践写法: 不要只写一个 return Text(...),一定要养成习惯处理三种状态:加载中、错误、正常显示

builder: (context, snapshot) {
  if (snapshot.hasError) return ErrorWidget();
  switch (snapshot.connectionState) {
    case ConnectionState.waiting: return LoadingWidget();
    case ConnectionState.active:
    case ConnectionState.done:
      return DataWidget(snapshot.data);
    default: return SizedBox();
  }
}

四、 新手必踩的超级大坑

在使用 StreamBuilder 时,90% 的新手会犯同一个错误:在 build 方法里创建 Stream

❌ 错误示范:

@override
Widget build(BuildContext context) {
  return StreamBuilder(
    // 错!每次父组件刷新,build 都会跑一遍
    // 这一行就会创建一个全新的 Stream!
    stream: createMyStream(), 
    builder: ...
  );
}

💥 后果: 每次你的界面刷新(比如键盘弹起、父组件 setState),createMyStream() 就会重新执行。 这就意味着:原本的连接断开了,一个新的连接建立了。 你会看到 Loading 转圈圈无限闪烁,或者倒计时明明走到 5 了,突然又变回 10 重新开始。

✅ 正确姿势: Stream 实例的创建必须在 build 方法之外

  1. 如果是 StatefulWidget,在 initState 里创建。
  2. 如果是 BLoC/Provider 模式,Stream 应该由业务逻辑类提供,UI 只负责引用。

小结

这一章我们见证了 Stream 与 Flutter UI 的完美融合。

  • StreamBuilder 是连接逻辑层与 UI 层的万能适配器
  • AsyncSnapshot 是携带数据的快递盒,我们要学会检查盒子的状态(Waiting/Active/Error)。
  • 铁律:永远不要在 build 方法里创建 Stream,那是“一次性筷子”,用完就丢,会导致状态重置。

到这里,关于 Stream 的基础、进阶和 UI 实战我们都讲完了。

但是,如果你正在开发一个中大型 APP,你会发现光有 Stream 还是不够。你需要一种架构模式,把 Stream 组织起来,让代码井井有条。 这就是 Flutter 官方推荐的 —— BLoC (Business Logic Component) 模式


第五章:Flutter 实战——BLoC 模式,给你的代码定规矩

经过前四章的学习,你手中已经握有了强大的武器:Stream,并且学会了 Stream 的所有招式(创建、变形、消费),是时候把它们组合成一套绝世武功了。

但你可能会发现一个新的问题:武器太灵活了,容易误伤自己。

如果你在 UI Widget 里随便创建 Controller,在 build 方法里随意处理数据,很快你的代码就会变成一碗“意大利面”——逻辑和 UI 纠缠不清,难以维护,难以测试。

为了解决这个问题,Flutter 社区诞生了一种基于 Stream 的架构模式:BLoC (Business Logic Component)

它的核心思想只有一句话:让 UI 只是 UI,让逻辑只是逻辑,两者通过 Stream 对话。

一、 BLoC 的“黑盒模型”

把 BLoC 想象成一台自动售货机

  1. 输入 (Input):你按下一个按钮(比如“购买可乐”)。这在 BLoC 里叫 Event (事件)
  2. 黑盒 (Processing):机器内部听到指令,检查库存,扣除余额,驱动机械臂。这就是 Business Logic (业务逻辑)
  3. 输出 (Output):机器吐出一听可乐,或者显示“余额不足”。这在 BLoC 里叫 State (状态)

关键规则:

  • UI 组件(Widget)绝对不允许直接修改数据。
  • UI 只能做一件事:往 BLoC 的 Sink 里扔事件
  • UI 只能听一件事:听 BLoC 的 Stream 里流出来的状态

二、 手写一个纯粹的 BLoC

在引入第三方库之前,我们先用原生 Dart 代码写一个 BLoC,你会发现它本质上就是我们第二章学的 StreamController 的封装。

我们来重构之前的“电子表”或“计数器”。

1. 定义 BLoC 类 (逻辑层)

import 'dart:async';

class CounterBloc {
  // 1. 状态流控制器 (Output):告诉 UI 当前是几
  // 使用广播流,允许多个页面同时监听
  final _stateController = StreamController<int>.broadcast();
  int _count = 0;

  // 2. 暴露给外部的 Stream (只读)
  Stream<int> get stream => _stateController.stream;

  // 3. 事件入口 (Input):UI 只能调这个方法
  void increment() {
    _count++;
    // 逻辑处理完,把新状态推出去
    _stateController.sink.add(_count); 
  }

  // 4. 资源释放
  void dispose() {
    _stateController.close();
  }
}

2. 在 UI 中使用 (视图层)

class CounterPage extends StatefulWidget {
  @override
  _CounterPageState createState() => _CounterPageState();
}

class _CounterPageState extends State<CounterPage> {
  final bloc = CounterBloc(); // 创建 BLoC

  @override
  void dispose() {
    bloc.dispose(); // 记得关流
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text("BLoC 模式")),
      body: Center(
        // 使用 StreamBuilder 监听 BLoC 的输出
        child: StreamBuilder<int>(
          stream: bloc.stream,
          initialData: 0,
          builder: (context, snapshot) {
            return Text(
              '${snapshot.data}', 
              style: TextStyle(fontSize: 40)
            );
          },
        ),
      ),
      floatingActionButton: FloatingActionButton(
        // UI 只负责触发动作
        onPressed: () => bloc.increment(),
        child: Icon(Icons.add),
      ),
    );
  }
}

看,代码清爽多了!

  • build 方法里没有任何 _count++ 这样的逻辑。
  • 逻辑代码全在 CounterBloc 里,你可以不依赖 Flutter UI 直接对 CounterBloc 写单元测试。

三、 进阶:为什么要用 flutter_bloc 库?

虽然手写 BLoC 帮我们理清了原理,但实际开发中,手动管理 StreamController 的关闭、手动定义 Sink 和 Stream 还是太繁琐了。

于是,大神 Felix Angelov 开源了 flutter_bloc 库,它把这套流程标准化了。

flutter_bloc 库中:

  1. 不再需要手动写 Controller:库帮你封装好了。
  2. 强制的 Event/State 定义:你必须先定义好所有的“动作”和“结果”,强制代码规范。
  3. BlocBuilder:它就是 StreamBuilder 的亲儿子,专门用来简化 BLoC 的监听。

四、 BLoC 的哲学意义

学习 BLoC,实际上是在学习一种 “单向数据流” (Unidirectional Data Flow) 的思想。

  • 没有 BLoC 时:数据满天飞,A 组件改了 B 的数据,C 组件又回调了 A 的方法。出了 Bug 根本找不到源头。
  • 有了 BLoC 后
  • 数据永远是 从上往下流 (Stream)。
  • 事件永远是 从下往上发 (Sink)。
  • 形成了一个完美的闭环。

五、 全剧终:Stream 学习之路

恭喜你!从第一章的 Future 单次请求,到 Stream 的传送带模型,再到 StreamController 的手动控制,最后上升到 BLoC 的架构模式。

你已经走完了 Dart 异步编程最核心的旅程。

回顾一下我们的成就:

  1. 底层原理:你懂了 yield 暂停机制,知道了异步不是魔法,是状态机的切换。
  2. 内存模型:你分清了单订阅和广播,知道如何避免内存泄漏。
  3. 工具箱:你掌握了 map, where, debounce 等操作符,能像做手术一样处理数据。
  4. 架构思维:你学会了用 Stream 将 UI 和逻辑彻底分离。