目录
介绍
一、什么是 BLoC?
BLoC(Business Logic Component,业务逻辑组件)由 Google 工程师 Paolo Soares 在 DartConf 2018 提出,最初是为了让 Flutter 与 AngularDart 共享业务逻辑代码。后来社区将其发展为一套完整的状态管理框架:bloc 与 flutter_bloc。
它的核心思想是:
UI 只负责展示和收集输入,所有业务逻辑都放进 BLoC;两者之间通过 Event(输入)和 State(输出)连接,数据永远单向流动。
用户交互
│
▼
Event(事件:发生了什么)
│
▼
Bloc(业务逻辑:怎么处理)
│
▼
State(状态:处理结果)
│
▼
UI 根据 State 重新渲染
二、为什么需要 BLoC?
① UI 与业务逻辑彻底解耦:Widget 不写 if/else 业务判断,只根据 State 渲染;逻辑层不 import 任何 Flutter 的 UI 库,甚至可以脱离 Flutter 单元测试。
② 所有变化皆可追溯:State 的每一次变化都由一个明确的 Event 触发,配合 BlocObserver 可以完整追踪「哪个事件 → 哪个状态」。
③ 天然适合异步:基于 Stream 实现,网络请求、定时器、输入防抖这类异步场景处理起来非常自然。
三、三方包组成
| 包名 | 职责 |
|---|---|
| bloc | 核心库:Bloc、Cubit、Event、State(纯 Dart,不依赖 Flutter) |
| flutter_bloc | Flutter 绑定:BlocProvider、BlocBuilder、BlocListener 等组件 |
核心概念
一、Event、State、Bloc
Event:UI 层发出的「输入」,描述发生了什么(如 CounterIncrementPressed),而不是要做什么。
State:Bloc 对外的「输出」,必须是不可变对象。
Bloc:接收 Event,执行业务逻辑,通过 emit 输出新 State。
CounterIncrementPressed(Event)
│
▼
on<CounterIncrementPressed>:count + 1(业务逻辑)
│
▼
emit(CounterState(count: 1))(输出新状态)
│
▼
BlocBuilder 收到新 State → 重建 UI
二、Cubit 与 Bloc 的区别
Cubit 是 Bloc 的简化版:没有 Event 层,直接调用方法 emit 新状态。
| 对比项 | Cubit | Bloc |
|---|---|---|
| 触发方式 | 调用方法 cubit.increment() |
发送事件 bloc.add(Event()) |
| 样板代码 | 少 | 多(需定义 Event 类) |
| 可追溯性 | 一般(只能看到方法调用) | 强(每个 State 变化都有对应 Event) |
| 事件治理 | 不支持 | 支持 EventTransformer(防抖、节流、并发、丢弃) |
| 适用场景 | 简单计数、开关切换 | 复杂业务、需要事件治理的场景 |
经验法则:
逻辑简单、事件少 → Cubit
逻辑复杂、需要防抖/节流 → Bloc
(两者可以混用,Cubit 随时可以平滑升级为 Bloc)
三、flutter_bloc 常用组件
| 组件 | 作用 | 对应 Provider 中的概念 |
|---|---|---|
| BlocProvider | 向下注入 Bloc/Cubit,并管理其生命周期 | ChangeNotifierProvider |
| BlocBuilder | 监听 State 变化并重建 | Consumer |
| BlocListener | 监听 State 变化执行一次性动作(弹窗、导航、Toast) | 无(Listener 语义) |
| BlocConsumer | BlocBuilder + BlocListener 合体 | — |
| BlocSelector | 只监听 State 中的部分字段 | Selector |
| context.read | 获取实例触发事件,不监听 | context.read |
| context.watch | 获取实例并监听重建 | context.watch |
基本用法
一、Cubit 版计数器(最简)
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
// ① 定义不可变 State
class CounterState {
const CounterState(this.count);
final int count;
}
// ② 定义 Cubit:持有初始状态,方法内 emit 新状态
class CounterCubit extends Cubit<CounterState> {
CounterCubit() : super(const CounterState(0));
void increment() => emit(CounterState(state.count + 1));
}
void main() {
runApp(
// ③ 注入 Cubit(create 会在移除时自动调用 close)
BlocProvider(
create: (_) => CounterCubit(),
child: const MyApp(),
),
);
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(title: const Text('Cubit Demo')),
body: Center(
// ④ BlocBuilder:只重建这一小块
child: BlocBuilder<CounterCubit, CounterState>(
builder: (_, state) => Text(
'${state.count}',
style: const TextStyle(fontSize: 48),
),
),
),
floatingActionButton: FloatingActionButton(
// ⑤ context.read:在回调中触发事件,不监听
onPressed: () => context.read<CounterCubit>().increment(),
child: const Icon(Icons.add),
),
),
);
}
}
二、Bloc 版计数器(事件驱动)
// ① 定义 Event(sealed + 工厂构造,Dart 3 穷举更安全)
sealed class CounterEvent {}
class CounterIncrementPressed extends CounterEvent {}
class CounterDecrementPressed extends CounterEvent {}
class CounterBloc extends Bloc<CounterEvent, CounterState> {
CounterBloc() : super(const CounterState(0)) {
// ② 注册事件处理器:on<事件>,内部 emit 新状态
// 这里是调用了on() 方法,注册了两个事件处理器
on<CounterIncrementPressed>(
(event, emit) => emit(CounterState(state.count + 1)),
);
on<CounterDecrementPressed>(
(event, emit) => emit(CounterState(state.count - 1)),
);
}
}
// 使用处:
// context.read<CounterBloc>().add(CounterIncrementPressed());
三、BlocListener:处理一次性副作用
State 变化需要重建 UI 用 BlocBuilder;需要执行弹窗、导航、Toast 这类「只做一次」的副作用用 BlocListener。
BlocListener<LoginBloc, LoginState>(
listener: (context, state) {
// 只在新状态满足条件时执行,不会随重建反复触发
if (state.isSuccess) {
Navigator.of(context).pushReplacementNamed('/home');
} else if (state.isFailure) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('登录失败')),
);
}
},
child: ...,
)
最佳实践
一、State 设计原则
1、State 必须不可变
永远不要修改 State 内部字段后重复 emit 同一个实例,应始终创建新对象。
同一个对象会被 emit 去重跳过。
2、State 表达业务语义,而不是一堆零散字段
不要用多个 bool 互相组合表示状态(isLoading + isSuccess + isFailure 会出现非法组合),用「语义化 State」表达互斥的业务阶段:
// ❌ 差:多个 bool 可能打架(isLoading 和 isSuccess 同时为 true)
class LoginState {
final bool isLoading;
final bool isSuccess;
final bool isFailure;
}
// ✅ 好:sealed 状态类,互斥且可穷举
sealed class LoginState {}
class LoginInitial extends LoginState {}
class LoginLoading extends LoginState {}
class LoginSuccess extends LoginState {}
class LoginFailure extends LoginState {
LoginFailure(this.message);
final String message;
}
3、继承 Equatable(或使用 Dart 3 的 sealed + 手写 ==)减少无效重建
BlocBuilder 默认对每个新 State 都会重建。State 实现 Equatable 后,内容不变就跳过重建:
class CounterState extends Equatable {
const CounterState(this.count);
final int count;
@override
List<Object?> get props => [count]; // 参与相等判断的字段
}
注意:List 等集合类型默认按引用比较,需要在 props 中做深度比较,否则 Equatable 失效。
二、Event 设计原则
1、Event 描述「发生了什么」,而不是「UI 要做什么」
// ❌ 差:描述 UI 动作,把展示逻辑泄漏进业务层
class ButtonClicked extends CounterEvent {}
// ✅ 好:描述业务事实,Bloc 决定怎么处理
class CounterIncrementPressed extends CounterEvent {}
class CounterRetryRequested extends CounterEvent {}
2、参数跟随 Event,而不是存在 Bloc 字段里
事件携带的输入(关键词、页码)作为 Event 的字段传入,保证 Bloc 自身无状态可变输入,事件可回放、可测试。
3、Event 用 sealed class 定义
编译期穷举 + 免手写 == / toString。
三、Bloc 拆分原则(企业级架构)
1、一个 Bloc 负责一个业务领域(feature),不要负责一个页面,更不要负责整个 App
页面只是业务领域的展示组合。按领域拆(登录、购物车、订单),页面可以同时订阅多个 Bloc。
2、生命周期不同的数据,不要放进同一个 Bloc
全局数据(用户信息、主题)用全局 Bloc;页面数据(表单、筛选条件)用页面级 Bloc,随页面销毁。
全局 Bloc:挂在 MaterialApp 之上,随 App 启动创建、App 退出才销毁,所有页面都能访问。
页面级 Bloc:挂在单个页面的 Widget 上,进入页面时创建,页面 pop 时由 BlocProvider 自动调用 close() 销毁。
| 全局 Bloc | 页面级 Bloc | |
|---|---|---|
| 挂载位置 | MaterialApp 之上 | 页面 Widget 内 |
| 创建时机 | App 启动 | 进入页面 |
| 销毁时机 | App 退出 | 页面 pop(自动 close) |
| 作用范围 | 全 App | 该页面子树 |
| 典型数据 | 用户信息、主题、购物车 | 表单、筛选、列表状态 |
3、高频变化的数据独立拆分
输入框防抖、进度条、滚动位置这类高频 emit,独立成小 Cubit,避免牵连大范围重建。
App
├── Global Bloc(随 App 存活)
│ ├── AuthBloc(登录态)
│ ├── ThemeBloc
│ └── CartBloc(购物车,跨页面)
│
└── Page Bloc(随页面销毁,BlocProvider 自动 close)
├── ProductListBloc(列表:加载、分页、筛选)
├── SearchCubit(搜索词,高频变化独立拆出)
└── OrderSubmitBloc(下单,用完即弃)
四、性能优化
1、buildWhen:过滤不需要的 State 变化
BlocBuilder<ProductBloc, ProductState>(
// 只有点击/加载相关字段变化时才重建
buildWhen: (prev, curr) => prev.isLoading != curr.isLoading,
builder: ...,
)
2、BlocSelector:只监听 State 的部分字段
State 很大但 Widget 只关心一个字段时使用(等价于 Provider 的 Selector)。
BlocSelector<CartBloc, CartState, int>(
selector: (state) => state.totalCount,
builder: (_, totalCount) => Badge(count: totalCount),
)
3、BlocProvider 放得越低越好,BlocBuilder 范围越小越好
与 Provider 同理:监听范围越小,重建越少;完全静态的子树用 const 或拆成独立 Widget。
五、资源管理与常见坑
1、重写 close() 释放资源
StreamSubscription、Timer、TextEditingController 都要在 close 中释放,防止内存泄漏:
class SearchBloc extends Bloc<SearchEvent, SearchState> {
SearchBloc() : super(const SearchState.initial()) {
on<KeywordChanged>(_onKeywordChanged);
}
final _debounce = Timer(const Duration(milliseconds: 300), () {});
@override
Future<void> close() {
_debounce.cancel(); // 必须释放
return super.close();
}
Future<void> _onKeywordChanged(
KeywordChanged event,
Emitter<SearchState> emit,
) async {
// ...
}
}
2、Bloc 中永远不要持有 BuildContext
导航、弹窗等 UI 副作用属于「对 State 变化的响应」,放到 UI 层的 BlocListener 里做;
Bloc 保持纯 Dart,才能脱离 Flutter 测试。
3、emit 只能在事件处理器执行期间调用
不要把 emit 捕获进 Timer、StreamSubscription 回调等处理器之外的地方异步调用,否则报错 emit was called after an event handler completed。订阅流应使用 await emit.forEach(...)。
4、数据访问通过 Repository,不要在 Bloc 里直接写网络请求
Bloc 编排业务(何时调、拿结果怎么转 State),Repository 负责数据来源(网络/本地)。依赖通过构造函数注入,方便替换与 Mock 测试:
class OrderBloc extends Bloc<OrderEvent, OrderState> {
OrderBloc({required OrderRepository repository})
: _repository = repository,
super(const OrderState.initial()) {
on<OrderSubmitted>(_onSubmitted);
}
final OrderRepository _repository;
Future<void> _onSubmitted(
OrderSubmitted event,
Emitter<OrderState> emit,
) async {
emit(const OrderState.loading());
try {
final order = await _repository.submit(event.params);
emit(OrderState.success(order));
} catch (e) {
emit(OrderState.failure(e.toString()));
}
}
}
5、Cubit 升级为 Bloc 的时机
当出现以下信号时,把 Cubit 重构为 Bloc:需要对事件防抖/节流(EventTransformer)、需要完整事件审计追踪、同一方法在不同入口需要不同语义。
行者常至,为者常成!