第一章: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" 的那一瞬间,发生了两件事:
- 结果交付:数据被发送给了等待者。
- 现场销毁:
fetchUser函数的栈帧 (Stack Frame) 被弹出、销毁。这个函数“死”了,它的生命周期彻底结束。
痛点来了: 如果你想要的是“连续的数据”呢? 比如,你不仅想知道“文件下载完了没有”,还想知道“现在下载了百分之几”。
- 如果你用
Future,你只能得到一张下载完成时的截图。 - 但你真正想要的是一段录像。
这时候,我们需要一个能“活着”并在时间轴上源源不断吐出数据的机制。
二、 Stream:时间轴上的传送带
如果说 Future 是静态的点,那么 Stream 就是动态的线。
你可以把 Stream 想象成回转寿司店里的一条自动传送带。
在这个模型里,有三个核心角色:
- Sink (入口/厨师):这是生产端。厨师(生成器)把一盘盘寿司(数据)按顺序放上传送带。
- Stream (管道):这是传送带本身。它负责搬运数据,它不关心谁吃,只负责转动。
- 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()。
小结
这一章我们完成了从“使用者”到“掌控者”的转变:
- 分清形态:
- 单订阅(默认):数据完整,一对一,错过即毁。
- 广播(Broadcast):实时性强,一对多,过时不候。
- 掌握控制:
- 使用
StreamController可以让我们在代码的任何地方主动地“推”数据,它是连接命令式代码(普通函数)和响应式代码(Stream)的桥梁。
了解了形态和控制,下一章我们将进入 Stream 最强大的领域 —— 数学般的魔法。
我们将探索如何像操作数组一样操作时间流:map、where、debounce(防抖)以及 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. 扁平化神技:expand 与 asyncExpand
这是一个高级但必用的操作符。
- 场景:Stream 里流过来的是“文件夹”,但监听者想要的是“文件”。 即:Stream 发出的每个数据,本身又包含了一组数据(Stream of List)。
- 解法:
expand会把“流过来的每一个元素”炸开,变成一堆元素,然后重新铺平在传送带上。
// 假设流过来的是:[1, 2], [3, 4]
stream
.expand((element) => element)
.listen(print);
// 输出:1, 2, 3, 4 (变成了扁平的流)
三、 终极武器:StreamTransformer
有时候,官方提供的 map、where 不够用了。
比如,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 面试必问的两大杀手锏:
- 防抖 (Debounce):
- 比喻:电梯关门。如果一直有人进电梯(事件一直来),电梯门就一直不关。只有当大家都不动了(间隔超过一定时间),电梯门才会关上(执行逻辑)。
- 用途:搜索框联想。打字停顿 500ms 后再请求 API。
- 节流 (Throttle):
- 比喻:机关枪射速限制。不管你扣扳机的手速有多快,子弹最快只能每秒发 10 发。
- 用途:防止按钮连点。
(注:RxDart 本质上就是把 StreamTransformer 封装好了给你用。)
小结
这一章我们把 Stream 从“传输工具”升级成了“处理工具”。
- 管道思维:用
map、where像搭积木一样处理数据。 - 独有技能:用
distinct过滤重复信号。 - 高级定制:用
transform处理复杂的数据转换(如二进制转文本)。
现在,我们有了数据源(Controller),有了处理逻辑(Operators),有了监听者(Listen)。
但是,在 Flutter 的 UI 代码里写 listen 和 setState 依然很痛苦,很容易忘掉 cancel 导致内存泄漏。
有没有一种 Widget,能直接把 Stream 插上去,它自己就会根据数据变来变去,还自动管理内存?
下一章,我们将介绍 Flutter 官方提供的终极组件 —— StreamBuilder,它是连接逻辑层与 UI 层的跨海大桥。
第四章:Flutter 实战——告别 setState,拥抱 StreamBuilder
在前面的章节中,我们在纯 Dart 环境下把 Stream 玩出了花。但当我们回到 Flutter 的 Widget 世界时,会遇到一个尴尬的现实。
痛点:手动管理的“地狱”
如果你不用专门的工具,想在界面上显示一个 Stream 的数据,你需要写大量的模版代码:
- 必须用
StatefulWidget。 - 在
initState里手动listen。 - 在回调里手动
setState触发刷新。 - 最要命的:必须在
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 在这一瞬间的体检报告”。它包含三个核心指标:
- ConnectionState (连接状态):
none: 没插网线(stream 为 null)。waiting: 插了网线,但第一个数据还没来(通常显示 Loading)。active: 数据正在源源不断地来(最主要的状态)。done: 传送带停了(Stream 关闭)。
- data (数据):
- 如果不为 null,说明这就是最新拿到的数据。
- 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 方法之外。
- 如果是
StatefulWidget,在initState里创建。 - 如果是 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 想象成一台自动售货机。
- 输入 (Input):你按下一个按钮(比如“购买可乐”)。这在 BLoC 里叫 Event (事件)。
- 黑盒 (Processing):机器内部听到指令,检查库存,扣除余额,驱动机械臂。这就是 Business Logic (业务逻辑)。
- 输出 (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 库中:
- 不再需要手动写 Controller:库帮你封装好了。
- 强制的 Event/State 定义:你必须先定义好所有的“动作”和“结果”,强制代码规范。
- BlocBuilder:它就是
StreamBuilder的亲儿子,专门用来简化 BLoC 的监听。
四、 BLoC 的哲学意义
学习 BLoC,实际上是在学习一种 “单向数据流” (Unidirectional Data Flow) 的思想。
- 没有 BLoC 时:数据满天飞,A 组件改了 B 的数据,C 组件又回调了 A 的方法。出了 Bug 根本找不到源头。
- 有了 BLoC 后:
- 数据永远是 从上往下流 (Stream)。
- 事件永远是 从下往上发 (Sink)。
- 形成了一个完美的闭环。
五、 全剧终:Stream 学习之路
恭喜你!从第一章的 Future 单次请求,到 Stream 的传送带模型,再到 StreamController 的手动控制,最后上升到 BLoC 的架构模式。
你已经走完了 Dart 异步编程最核心的旅程。
回顾一下我们的成就:
- 底层原理:你懂了
yield暂停机制,知道了异步不是魔法,是状态机的切换。 - 内存模型:你分清了单订阅和广播,知道如何避免内存泄漏。
- 工具箱:你掌握了
map,where,debounce等操作符,能像做手术一样处理数据。 - 架构思维:你学会了用 Stream 将 UI 和逻辑彻底分离。