本文定位:最少代码实践 + 真实踩坑复盘。不讲源码原理,不堆概念,只带你用一个计数器 Demo 跑通 Bloc 全链路,并且亲手解决一个"看起来很玄学"的 Provider 找不到的问题。
一、为什么还要学 Bloc?都 2026 年了 Riverpod 不香吗?
先说结论:香,但 Bloc 的思维模型值得你掌握。
Bloc 是 Flutter 生态里最早一批"正经"的状态管理方案,它把"事件驱动 + 单向数据流"这套范式贯彻得最彻底。即使你项目里用的是 Riverpod,理解 Bloc 的 Event → State 模型也能帮你更好地设计状态流转。
两者核心差异一句话:
| Bloc | Riverpod | |
|---|---|---|
| 状态变更入口 | 必须通过 add(Event) |
直接调 notifier 方法 |
| 约束强度 | 强(只能 emit 新状态) |
弱(可直接 state = ...) |
| 可追踪性 | 极高(每个 Event 可日志化) | 中(依赖 ProviderObserver) |
| 样板代码 | 较多 | 较少 |
Bloc 适合: 状态流转复杂、需要审计/可回放、团队规模大需要强约束的场景。
Riverpod 适合: 追求简洁、依赖注入需求强、编译期安全优先的场景。
二、最少代码跑通 Bloc
我们用一个经典的计数器来演示,只需要 2 个文件 + 1 个依赖。
2.1 添加依赖
yaml
# pubspec.yaml
dependencies:
flutter_bloc: ^8.1.6
bash
flutter pub get
2.2 核心三要素:Event / State / Bloc
新建 lib/bloc_demo/counter_bloc.dart:
dart
import 'package:bloc/bloc.dart';
// ========== 事件(Event):用户意图 ==========
sealed class CounterEvent {}
final class Increment extends CounterEvent {}
final class Decrement extends CounterEvent {}
// ========== 状态(State):UI 可观察的数据 ==========
class CounterState {
const CounterState(this.count);
final int count;
}
// ========== Bloc:事件 → 状态 的转换引擎 ==========
class CounterBloc extends Bloc<CounterEvent, CounterState> {
CounterBloc() : super(const CounterState(0)) {
on<Increment>((event, emit) => emit(CounterState(state.count + 1)));
on<Decrement>((event, emit) => emit(CounterState(state.count - 1)));
}
}
这里有几个关键点:
sealed class+final class:Dart 3.0+ 推荐写法,配合on<Event>做事件分发,比传统Equatable继承更轻量。on<Event>是唯一入口 :所有状态变更必须通过emit,Bloc 不允许直接修改state,这就是它"单向数据流 + 可追踪"的根基。super(const CounterState(0)):初始状态必须在构造时声明。
2.3 UI 页面
新建 lib/bloc_demo/bloc_demo_page.dart:
dart
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'counter_bloc.dart';
class BlocDemoPage extends StatelessWidget {
const BlocDemoPage({super.key});
static const routeName = '/bloc-demo';
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (_) => CounterBloc(),
child: Scaffold(
appBar: AppBar(title: const Text('Bloc 最少代码实践')),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Text('你点了这么多次:'),
BlocBuilder<CounterBloc, CounterState>(
builder: (context, state) => Text(
'${state.count}',
style: Theme.of(context).textTheme.headlineMedium,
),
),
],
),
),
floatingActionButton: Builder( // ⚠️ 注意这个 Builder
builder: (context) => Column(
mainAxisAlignment: MainAxisAlignment.end,
children: [
FloatingActionButton(
onPressed: () => context.read<CounterBloc>().add(Increment()),
child: const Icon(Icons.add),
),
const SizedBox(height: 8),
FloatingActionButton(
onPressed: () => context.read<CounterBloc>().add(Decrement()),
child: const Icon(Icons.remove),
),
],
),
),
),
);
}
}
跑起来,你就拥有了一个完整的 Bloc 计数器。🎉
三、踩坑复盘:Could not find the correct Provider
3.1 问题现象
代码写完运行,一点击按钮就报错:
arduino
Could not find the correct Provider<CounterBloc> above this BlocDemoPage Widget
诡异的是------数字显示是正常的,只有按钮点击报错。
3.2 根因:BuildContext 的作用域陷阱
这是 Flutter 中 BuildContext 作用域的经典问题。我们来看 widget 树:
scss
BlocDemoPage.build(context) ← 外层 context(在 BlocProvider 之上)
└── BlocProvider(create: ...) ← Provider 注入点
└── Scaffold
├── body: BlocBuilder ← ✅ builder 回调有子树 context,能找到
└── floatingActionButton
└── onPressed: context.read() ← ❌ 用的是外层 context
核心原因: floatingActionButton 是 Scaffold 的一个属性,它直接闭合了 build(BuildContext context) 传入的外层 context。这个 context 在 widget 树中位于 BlocProvider 的上方 ,所以 context.read<CounterBloc>() 向上查找时越过了注入点,自然找不到。
那为什么 BlocBuilder 能正常工作?因为它的 builder: (context, state) 回调提供了一个位于子树内的新 context ,这个新 context 向上查找时能命中 BlocProvider。
3.3 修复方案一:套一层 Builder(最小侵入)
dart
floatingActionButton: Builder(
builder: (context) => Column( // 这个 context 在 BlocProvider 子树内
children: [
FloatingActionButton(
onPressed: () => context.read<CounterBloc>().add(Increment()),
...
),
],
),
),
Builder 会在当前位置插入一个新的 BuildContext,这个 context 位于 BlocProvider 的子树中,向上查找时能命中注入的 CounterBloc。
3.4 修复方案二:把 Provider 提到路由层(更工程化)
如果页面结构复杂,到处包 Builder 不优雅,可以直接在路由注册时注入:
dart
// main.dart
BlocDemoPage.routeName: (_) => BlocProvider(
create: (_) => CounterBloc(),
child: const BlocDemoPage(),
),
这样 BlocDemoPage 内部就不需要再写 BlocProvider,整个页面的 context(包括 FAB)都在 Provider 之下,不会再有作用域问题。
两种方案选哪种?
- Demo / 单页验证:方案一,改动小
- 正式工程:方案二,Provider 注入点集中在路由层,便于管理和测试
四、Bloc 的心智模型总结
跑通这个 Demo 后,你应该记住这张图:
scss
┌─────────┐ add(Event) ┌──────────┐ emit(State) ┌──────────┐
│ UI │ ──────────────▶│ Bloc │ ───────────────▶│ State │
└─────────┘ └──────────┘ └──────────┘
▲ │
│ BlocBuilder / BlocListener │
└─────────────────────────────────────────────────────────┘
- UI 永远不直接改状态,只发 Event
- Bloc 是唯一的状态生产者 ,所有变更走
emit - State 是不可变的,每次都是新对象
- UI 通过
BlocBuilder响应式订阅,状态变了自动重建
这套模型的好处是:任何一个状态变化都能追溯到具体的 Event,调试时只需要看 Bloc 的事件流就能定位问题。对于复杂业务(比如电商下单、支付流程),这种可追溯性价值极高。
五、给 Riverpod 用户的对标速查
如果你和我一样主力用 Riverpod,这张对照表能帮你快速建立映射:
| Bloc 概念 | Riverpod 对标 |
|---|---|
Event |
不存在对应物(直接调 notifier 方法) |
State |
StateProvider 的 state / Notifier 的 state |
Bloc |
Notifier / AsyncNotifier |
BlocProvider |
ProviderScope 内的 Provider |
BlocBuilder |
ref.watch(provider) |
context.read().add(Event) |
ref.read(provider.notifier).method() |
BlocListener |
ref.listen(provider, ...) |
六、下一步可以怎么扩展?
这个计数器只是 Bloc 的"Hello World",真实场景中你会遇到:
- 异步状态 :比如加载数据,用
emit配合isLoading状态,或者用Future+AsyncValue思路 - 状态去重 :给
State加Equatable,相同状态不触发重建 - 多事件组合 :用
on<Event>.debounce()做防抖(搜索输入框神器) - Bloc 间通信 :
BlocListener监听一个 Bloc,add事件到另一个 Bloc - 测试 :
bloc_test包可以非常方便地写 "given event, expect state" 的单测
写在最后
Bloc 不是银弹,它的样板代码确实比 Riverpod 多。但它强约束的 Event → State 模型,在团队协作和复杂业务场景下能带来更好的可维护性和可调试性。
建议: 如果你正在选型,先用 Riverpod 快速迭代,遇到状态流转复杂、需要审计的模块时,再局部引入 Bloc。两者不冲突,关键是理解背后的"单向数据流"思想。
如果你在 Bloc 实践中也遇到过什么坑,欢迎评论区交流~
