BLoC 模式深度解析
BLoC 用 Stream 把事件与状态解耦:讲清 Event→Bloc→State 的单向数据流与 Stream 中转。 掌握后能说清「BLoC 和 Provider 的核心差异」及适用场景。
一句话概括
BLoC = Events In, States Out。每一次状态变化都有明确的事件来源——这在 Flutter 状态管理方案里是独一份。Provider 告诉你 “状态变了”,BLoC 告诉你 “谁触发的变化、从哪个状态变到了哪个状态”。
核心知识点
1. 三层模型:Event → Bloc → State
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// 事件:描述"发生了什么"
class LoginButtonPressed {
final String email, password;
const LoginButtonPressed(this.email, this.password);
}
// 状态:描述"界面长什么样"
abstract class LoginState {}
class LoginInitial extends LoginState {}
class LoginLoading extends LoginState {}
class LoginSuccess extends LoginState { final String token; LoginSuccess(this.token); }
class LoginFailure extends LoginState { final String msg; LoginFailure(this.msg); }
// BLoC:事件→状态的转换逻辑(纯 Dart,零 UI 依赖)
class LoginBloc extends Bloc<LoginButtonPressed, LoginState> {
final AuthRepo repo;
LoginBloc(this.repo) : super(LoginInitial()) {
on<LoginButtonPressed>((event, emit) async {
emit(LoginLoading());
try {
final token = await repo.login(event.email, event.password);
emit(LoginSuccess(token));
} catch (e) {
emit(LoginFailure(e.toString()));
}
});
}
}
关键:事件和状态都用类来表示,而不是原始类型。这是 BLoC 的最大设计原则——类型系统帮你区分 10 种不同的 “loading” 状态。
2. BlocBuilder / BlocListener / BlocConsumer 三兄弟
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// BlocBuilder:状态映射到 UI(会重建)
BlocBuilder<LoginBloc, LoginState>(
builder: (context, state) => switch (state) {
LoginLoading() => const CircularProgressIndicator(),
LoginSuccess(:final token) => Text('登录成功, token: $token'),
LoginFailure(:final msg) => Text('出错: $msg'),
_ => const LoginForm(),
},
);
// BlocListener:一次性副作用(不重建 UI)
BlocListener<LoginBloc, LoginState>(
listener: (context, state) {
if (state is LoginSuccess) Navigator.pushReplacementNamed(context, '/home');
if (state is LoginFailure) ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(state.msg)));
},
child: const LoginForm(),
);
// BlocConsumer:既要重建 UI 又要副作用,合二为一
BlocConsumer<LoginBloc, LoginState>(
listener: /* 同上 */,
builder: /* 同上 */,
);
记法:Builder 管 UI,Listener 管动作。两者互斥但 BlocConsumer 把它俩装一块了。
3. 事件转换器:用 debounce 避免浪费请求
1
2
3
4
5
6
7
class SearchBloc extends Bloc<SearchEvent, SearchState> {
SearchBloc() : super(SearchInitial()) {
on<QueryChanged>(_onQueryChanged,
transformer: debounce(const Duration(milliseconds: 300)), // BLoC 内置
);
}
}
debounce transformer 直接消灭了搜索时 “每按一个键发一次请求” 的问题。比在 UI 层手动写 Timer 清爽太多。
4. BLoC 最大的卖点:测试不需要 Widget
1
2
3
4
5
6
7
8
9
10
11
12
13
blocTest<LoginBloc, LoginState>(
'登录成功: Initial → Loading → Success',
build: () => LoginBloc(MockAuthRepo()..mockLogin(returns: 'token123')),
act: (bloc) => bloc.add(LoginButtonPressed('a@b.com', '123456')),
expect: () => [LoginLoading(), LoginSuccess('token123')],
);
blocTest<LoginBloc, LoginState>(
'登录失败: Initial → Loading → Failure',
build: () => LoginBloc(MockAuthRepo()..mockLogin(throws: AuthException('密码错误'))),
act: (bloc) => bloc.add(LoginButtonPressed('a@b.com', 'wrong')),
expect: () => [LoginLoading(), LoginFailure('密码错误')],
);
blocTest 接收一个 BLoC 构造函数、一个事件、然后断言状态序列。整个过程不碰 Widget 树,不碰 pumpAndSettle,毫秒级跑完。
5. BLoC 底层就是两个 StreamController
1
2
3
4
5
6
7
8
9
10
11
// BLoC 内部伪代码
abstract class Bloc<Event, State> {
final _eventController = StreamController<Event>.broadcast();
final _stateController = StreamController<State>.broadcast();
Bloc() { _eventController.stream.listen(_handleEvent); }
void add(Event e) => _eventController.add(e);
void emit(State s) => _stateController.add(s);
Stream<State> get stream => _stateController.stream;
}
一个 StreamController 收事件,一个 StreamController 发状态。中间是你的 on<T>(handler) 注册的函数。
其实你每天都在用
- 搜索框的实时补全 — 一边打字一边发请求 → BLoC debounce transformer 让只有最后一次敲键才触发请求
- 登录/注册流程 — Initial → Loading → Success/Failure 三态切换,BLoC 的标准模板
- 分页加载更多 — 滚动到底部发
LoadMore事件,BLoC 追加新数据到列表状态 - 多步骤表单 — 每个步骤一个事件,状态里记录步骤序号和数据,BLoC 天然适合 wizard 流程
- WebSocket 实时推送 — Stream 直接对接,价格变动事件 → BLoC → UI 刷新
常见误解(FAQ)
❌ 误区:「BLoC 模板代码太多,写起来很累」 确实比 Provider 多。但这堆 “多余代码” 换来的是:事件回溯、测试不需要 Widget、状态不会在背后被修改——三个字:可维护。小型 CRUD 项目别上 BLoC,中大型多人协作项目别不上 BLoC。
**❌ 误区:「on
里可以在 await 之后 emit 任意多次」** BLoC 关闭后 emit 会抛异常。比如用户在 loading 期间关闭了页面,BlocProvider 自动 dispose BLoC,你的 `await repo.login()` 回来再 `emit(LoginSuccess(...))` —— booom。正确做法:`if (!isClosed) emit(...)`。 ❌ 误区:「BlocBuilder 只是一个更啰嗦的 setState」
setState重建整个 StatefulWidget 的 build,BlocBuilder 只重建 builder 回调的内容。前者像全屏刷新,后者像精准更新一个 cell。❌ 误区:「BLoC 和 Provider 互斥,不能一起用」 它们可以共存。Provider 管配置类、主题、简单的值;BLoC 管复杂业务流程。
MultiBlocProvider旁边放个Provider<bool>.value(value: isDark)—— 完全没问题。
一句话总结
BLoC 的强项不是好用,是好查——当产品问 “为什么用户页面显示了这个错误状态”,你能顺着事件日志找到哪个事件触发的哪个状态。Provider 给了你速度,BLoC 给了你审计。