在进入公司之前,我对 Dart 语言和 Flutter 框架完全一无所知。
入职第一天,专家简单培训了一下,大概意思是:“这是个跨平台框架,一套代码能跑 iOS、Android、Web。”
我当时心想:无所谓了,反正都是从零开始,学什么都一样。
结果现实很快打脸——我连 Dart 语法都没系统学过,就直接被扔进了项目里。
专家轻描淡写地说:“语言都是互通的,语法看看就会了。”
Ok Ok,大佬说话就是这么轻飘飘、模模糊糊。
更懵的是 BLoC 设计模式——这词我听都没听过。
UI 和逻辑分开?中间人?事件驱动?状态管理?
一开始看项目代码,满屏的 BlocProvider、BlocBuilder、emit()、copyWith(),像天书一样。
好在边看边改、边改边问,总算把现有页面搞明白了。
但一到自己写新功能,又开始犯迷糊:
“这个逻辑该放 Cubit 还是放 View?”
“State 怎么定义才规范?”
“为什么不能直接在页面里调 API?”
于是趁着这次机会,我决定把 BLoC 的核心思想和我们项目中的实际用法,系统地记录下来。
既是分享,也是给自己备份——下次再忘,翻翻这篇文章就行。
那么,BLoC 到底是什么?
BLoC(Business Logic Component,业务逻辑组件)的核心思想很简单:
把 UI 和业务逻辑彻底分开。
在 Flutter 应用中,BLoC 就像一个“中间人”:
- UI 层只负责展示和收集用户操作;
- 所有数据处理、规则判断、网络请求等业务逻辑,全部交给 BLoC;
- BLoC 处理完后,告诉 UI:“现在该显示什么了”。
这种分离带来了三大实实在在的好处:
- 可维护性高
UI 只管“怎么画”,逻辑全在 BLoC 里。找 Bug、加功能时,不用在几百行 build 方法里翻来覆去,结构一目了然。 - 可测试性强
BLoC 本质就是一个普通的 Dart 类,不依赖BuildContext或任何 UI 组件。你可以像测试普通函数一样,轻松为它写单元测试。 - 可复用性好
一个写好的 BLoC,稍作封装,就能在多个页面甚至不同 App 中复用——比如登录逻辑、设备状态管理等。
传统 BLoC vs 我们项目的简化版
理论上,BLoC 包含三个核心概念:
- Event:UI 发给 BLoC 的“指令”(如
LoginButtonPressed) - State:BLoC 返回给 UI 的“当前状态”(如
LoadingState) - Bloc:接收 Event、处理逻辑、发射 State 的核心组件
但在我们实际项目中,为了降低复杂度、提升开发效率,团队普遍采用 Cubit(BLoC 的简化版),并将其拆解为 四个更清晰的角色:
| 文件 | 职责 | 关键点 |
|---|---|---|
| State | 存数据 | final 字段 + copyWith() |
| Cubit | 改数据 | 方法 + emit() |
| View | 展示数据 | BlocBuilder 监听 |
| 页面入口 | 组装 | BlocProvider 提供 Cubit |
💡 为什么用 Cubit 而不是 Bloc?
因为我们大多数交互是“方法调用式”的(比如点击按钮变色、加载列表),不需要复杂的事件流。Cubit 更简洁,学习成本更低,足够应对工业软件的常见场景。
接下来,我会详细拆解这四个角色的规范写法、设计原理和常见误区,全是我在踩坑后总结出的经验。
如果你也和我一样,是从零开始接触 BLoC,希望这篇总结能帮你少走弯路。
State:只负责“存数据”,而且必须是“不可变”的
刚接触 State 时,我第一反应是:“不就是个普通 class 吗?随便加字段就行。”
结果一运行就报错,或者状态更新没反应。后来才明白:State 的核心原则是“不可变”(immutable) 。
✅ 规范写法(必须三要素)
class CubeColorState {
final Color color; // 1. 字段必须是 final
const CubeColorState({this.color = Colors.blue}); // 2. 构造函数用 const
CubeColorState copyWith({Color? color}) { // 3. 必须提供 copyWith
return CubeColorState(color: color ?? this.color);
}
}
❓ 为什么这么麻烦?
final:确保状态一旦创建就不能被修改。避免“意外改了某个字段却没触发 UI 更新”。const:提升性能,相同状态可复用。copyWith:因为不能直接改字段,所以要用它“复制一份新状态”。这是 BLoC 响应式更新的关键!
💡 我的踩坑:
一开始我把color写成Color color;(非 final),然后在 Cubit 里直接state.color = red;—— 结果 UI 完全不动!
后来才知道:BLoC 只监听“整个 State 对象是否变化”,而不是“内部字段是否变化” 。
Cubit:“业务逻辑大脑”
如果说 State 是数据容器,那 Cubit 就是唯一能改变这个容器的人。
✅ 规范写法
class CubeColorCubit extends Cubit<CubeColorState> {
CubeColorCubit() : super(const CubeColorState()); // 初始状态
void changeColor(Color newColor) {
emit(state.copyWith(color: newColor)); // 发射新状态
}
}
🚫 常见错误(我都犯过)
| 错误写法 | 问题 |
|---|---|
void changeColor(Color c) { state.color = c; } | 没有 emit,UI 不会更新 |
emit(CubeColorState(color: newColor)); | 没用 copyWith,丢失其他字段(如果 State 有多个字段) |
在方法里用 BuildContext | Cubit 不该知道 UI 存在! |
💡 为什么叫 “Cubit” 而不是 “Bloc”?
- Bloc 需要定义
Event,通过mapEventToState处理,适合复杂事件流(如网络请求 + 重试 + 缓存)。 - Cubit 直接暴露方法(如
changeColor()),调用更直观,适合 90% 的 UI 交互场景(按钮点击、开关切换、颜色选择等)。
✅ 我们的项目选择 Cubit,就是因为:简单、直接、少写样板代码。
View:只负责“看数据”,别的什么都不干
View 是纯展示层,它不应该包含任何业务逻辑,甚至连“怎么变色”都不该知道。
✅ 规范写法
class CubeColorView extends StatelessWidget {
const CubeColorView({super.key});
@override
Widget build(BuildContext context) {
return BlocBuilder<CubeColorCubit, CubeColorState>(
builder: (context, state) {
return Container(
width: 100,
height: 100,
color: state.color, // 只读取 state,不做判断
);
},
);
}
}
🚫 绝对不要在 View 里做这些事:
- 调用 API
- 写 if-else 业务判断(比如 “如果是红色就弹窗”)
- 直接修改状态(
context.read<Cubit>().xxx应该只在用户操作时触发)
💡 View 的唯一职责:把 state 转成 UI。
页面入口:用 BlocProvider “串起来”
最后一步,把 Cubit 注入到 widget 树中,让 View 能“看到”它。
✅ 规范写法
class CubeColorPage extends StatelessWidget {
const CubeColorPage({super.key});
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (context) => CubeColorCubit(), // 创建 Cubit
child: const CubeColorView(),
);
}
}
⚠️ 关键细节
BlocProvider必须放在View的父级,否则BlocBuilder找不到 Cubit。- 不要在这里传
BuildContext给 Cubit(Cubit 不需要 context!)。
完整数据流:从点击到变色
现在,把四部分串起来,看一次完整交互:
- 用户点击按钮(在view中) → 调用
context.read<CubeColorCubit>().changeColor(Colors.red); CubeColorCubit.changeColor()执行 →emit(state.copyWith(color: red))- 新
CubeColorState被发射 BlocBuilder监听到状态变化 → 重新执行builderContainer的color更新为红色 → UI 刷新
🔁 整个过程:UI → Cubit → State → UI,形成闭环,且各司其职。
为什么这套模式值得坚持?
刚开始我觉得:“好麻烦啊,直接在 StatefulWidget 里 setState 不就行了?”
但随着页面变多、逻辑变复杂,我才发现 BLoC 的威力:
- 改颜色逻辑只在一个地方(Cubit),不用在 10 个页面里复制粘贴;
- 测试变色功能?只要测
Cubit.changeColor(),不用启动整个 App; - 新人接手?看到
State就知道页面有哪些状态,看到Cubit就知道能做什么操作。
✅ BLoC 不是为了炫技,而是为了“让代码活得更久”。
一个完整的页面的代码结构就是
终极总结:BLoC 是一种“分类”的思想
BLoC 本质上不是一套强制规则,而是一种清晰的职责分类思想——把 UI、数据、逻辑拆开,各干各的,互不干扰。
它的核心由四个角色构成:
State是数据的快照:只存状态,字段必须是final,通过copyWith()创建新版本。Cubit是逻辑的执行者:接收方法调用,处理业务,用emit()发出新状态。View是状态的观察者:只读取State,用BlocBuilder响应变化,绝不掺杂逻辑。- 主入口(页面) 是依赖的注入点:通过
BlocProvider将 Cubit 提供给子树,完成组装。
🔑 四个关键元素,必须写在正确的位置:
| 元素 | 写在哪个文件/类中 | 为什么 |
|---|---|---|
BlocProvider | 页面入口文件(如 cube_page.dart) | 在 widget 树中提供 Cubit 实例,让子组件能访问它 |
BlocBuilder | View 文件(如 cube_view.dart) | 监听 State 变化,并在状态更新时重建对应 UI |
emit() | Cubit 文件(如 cube_cubit.dart) | Cubit 唯一更新状态的方式,发射一个全新的 State |
copyWith() | State 文件(如 cube_state.dart) | 安全创建新 State 的方法,保留未修改字段的原始值 |
⚠️ 重要原则:每次 emit 都是整个 State 对象
当你调用:
emit(state.copyWith(color: Colors.red));
即使只改了一个字段,整个 State 对象也被替换了,所有监听该 State 的 UI 都会重建。
💡 性能优化思路:按 UI 粒度拆分 State
如果某个组件需要大量独立数据(比如折线图有上千个点),不要把所有数据塞进同一个 State!
而是为它单独创建 Cubit + State,这样:
- 立方体变色 → 折线图不重建
- 图表数据更新 → 立方体不重绘
✅ BLoC 的真正威力,不在于“用了它”,而在于“用对了粒度”。