《一个 Dart 零基础新人的 BLoC 入门实录》

0 阅读8分钟

在进入公司之前,我对 Dart 语言和 Flutter 框架完全一无所知
入职第一天,专家简单培训了一下,大概意思是:“这是个跨平台框架,一套代码能跑 iOS、Android、Web。”
我当时心想:无所谓了,反正都是从零开始,学什么都一样。

结果现实很快打脸——我连 Dart 语法都没系统学过,就直接被扔进了项目里
专家轻描淡写地说:“语言都是互通的,语法看看就会了。”
Ok Ok,大佬说话就是这么轻飘飘、模模糊糊。

更懵的是 BLoC 设计模式——这词我听都没听过。
UI 和逻辑分开?中间人?事件驱动?状态管理?
一开始看项目代码,满屏的 BlocProviderBlocBuilderemit()copyWith(),像天书一样。

好在边看边改、边改边问,总算把现有页面搞明白了。
但一到自己写新功能,又开始犯迷糊:

“这个逻辑该放 Cubit 还是放 View?”
“State 怎么定义才规范?”
“为什么不能直接在页面里调 API?”

于是趁着这次机会,我决定把 BLoC 的核心思想和我们项目中的实际用法,系统地记录下来
既是分享,也是给自己备份——下次再忘,翻翻这篇文章就行。


那么,BLoC 到底是什么?

BLoC(Business Logic Component,业务逻辑组件)的核心思想很简单:

把 UI 和业务逻辑彻底分开

在 Flutter 应用中,BLoC 就像一个“中间人”:

  • UI 层只负责展示收集用户操作
  • 所有数据处理、规则判断、网络请求等业务逻辑,全部交给 BLoC;
  • BLoC 处理完后,告诉 UI:“现在该显示什么了”。

这种分离带来了三大实实在在的好处:

  1. 可维护性高
    UI 只管“怎么画”,逻辑全在 BLoC 里。找 Bug、加功能时,不用在几百行 build 方法里翻来覆去,结构一目了然。
  2. 可测试性强
    BLoC 本质就是一个普通的 Dart 类,不依赖 BuildContext 或任何 UI 组件。你可以像测试普通函数一样,轻松为它写单元测试。
  3. 可复用性好
    一个写好的 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 有多个字段)
在方法里用 BuildContextCubit 不该知道 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!)。

完整数据流:从点击到变色

现在,把四部分串起来,看一次完整交互:

  1. 用户点击按钮(在view中) → 调用 context.read<CubeColorCubit>().changeColor(Colors.red);
  2. CubeColorCubit.changeColor() 执行 → emit(state.copyWith(color: red))
  3. CubeColorState 被发射
  4. BlocBuilder 监听到状态变化 → 重新执行 builder
  5. Containercolor 更新为红色 → UI 刷新

🔁 整个过程:UI → Cubit → State → UI,形成闭环,且各司其职。


为什么这套模式值得坚持?

刚开始我觉得:“好麻烦啊,直接在 StatefulWidget 里 setState 不就行了?”
但随着页面变多、逻辑变复杂,我才发现 BLoC 的威力:

  • 改颜色逻辑只在一个地方(Cubit),不用在 10 个页面里复制粘贴;
  • 测试变色功能?只要测 Cubit.changeColor(),不用启动整个 App;
  • 新人接手?看到 State 就知道页面有哪些状态,看到 Cubit 就知道能做什么操作。

BLoC 不是为了炫技,而是为了“让代码活得更久”。


一个完整的页面的代码结构就是

image.png


终极总结:BLoC 是一种“分类”的思想

BLoC 本质上不是一套强制规则,而是一种清晰的职责分类思想——把 UI、数据、逻辑拆开,各干各的,互不干扰。

它的核心由四个角色构成:

  • State数据的快照:只存状态,字段必须是 final,通过 copyWith() 创建新版本。
  • Cubit逻辑的执行者:接收方法调用,处理业务,用 emit() 发出新状态。
  • View状态的观察者:只读取 State,用 BlocBuilder 响应变化,绝不掺杂逻辑。
  • 主入口(页面)依赖的注入点:通过 BlocProvider 将 Cubit 提供给子树,完成组装。

🔑 四个关键元素,必须写在正确的位置:

元素写在哪个文件/类中为什么
BlocProvider页面入口文件(如 cube_page.dart在 widget 树中提供 Cubit 实例,让子组件能访问它
BlocBuilderView 文件(如 cube_view.dart监听 State 变化,并在状态更新时重建对应 UI
emit()Cubit 文件(如 cube_cubit.dartCubit 唯一更新状态的方式,发射一个全新的 State
copyWith()State 文件(如 cube_state.dart安全创建新 State 的方法,保留未修改字段的原始值

⚠️ 重要原则:每次 emit 都是整个 State 对象

当你调用:

emit(state.copyWith(color: Colors.red));

即使只改了一个字段,整个 State 对象也被替换了,所有监听该 State 的 UI 都会重建。

💡 性能优化思路:按 UI 粒度拆分 State

如果某个组件需要大量独立数据(比如折线图有上千个点),不要把所有数据塞进同一个 State
而是为它单独创建 Cubit + State,这样:

  • 立方体变色 → 折线图不重建
  • 图表数据更新 → 立方体不重绘

BLoC 的真正威力,不在于“用了它”,而在于“用对了粒度”。