【Flutter Bloc 从 0 到 1】用最少代码跑通 Bloc

0 阅读5分钟

本文定位:最少代码实践 + 真实踩坑复盘。不讲源码原理,不堆概念,只带你用一个计数器 Demo 跑通 Bloc 全链路,并且亲手解决一个"看起来很玄学"的 Provider 找不到的问题。


一、为什么还要学 Bloc?都 2026 年了 Riverpod 不香吗?

先说结论:香,但 Bloc 的思维模型值得你掌握。

Bloc 是 Flutter 生态里最早一批"正经"的状态管理方案,它把"事件驱动 + 单向数据流"这套范式贯彻得最彻底。即使你项目里用的是 Riverpod,理解 Bloc 的 Event → State 模型也能帮你更好地设计状态流转。

两者核心差异一句话:

BlocRiverpod
状态变更入口必须通过 add(Event)直接调 notifier 方法
约束强度强(只能 emit 新状态)弱(可直接 state = ...)
可追踪性极高(每个 Event 可日志化)中(依赖 ProviderObserver)
样板代码较多较少

Bloc 适合: 状态流转复杂、需要审计/可回放、团队规模大需要强约束的场景。
Riverpod 适合: 追求简洁、依赖注入需求强、编译期安全优先的场景。


二、最少代码跑通 Bloc

我们用一个经典的计数器来演示,只需要 2 个文件 + 1 个依赖。

2.1 添加依赖

# pubspec.yaml
dependencies:
  flutter_bloc: ^8.1.6
flutter pub get

2.2 核心三要素:Event / State / Bloc

新建 lib/bloc_demo/counter_bloc.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)));
  }
}

这里有几个关键点:

  1. sealed class + final class:Dart 3.0+ 推荐写法,配合 on<Event> 做事件分发,比传统 Equatable 继承更轻量。
  2. on<Event> 是唯一入口:所有状态变更必须通过 emit,Bloc 不允许直接修改 state,这就是它"单向数据流 + 可追踪"的根基。
  3. super(const CounterState(0)):初始状态必须在构造时声明。

2.3 UI 页面

新建 lib/bloc_demo/bloc_demo_page.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 问题现象

代码写完运行,一点击按钮就报错:

Could not find the correct Provider<CounterBloc> above this BlocDemoPage Widget

诡异的是——数字显示是正常的,只有按钮点击报错。

3.2 根因:BuildContext 的作用域陷阱

这是 Flutter 中 BuildContext 作用域的经典问题。我们来看 widget 树:

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(最小侵入)

floatingActionButton: Builder(
  builder: (context) => Column(  // 这个 context 在 BlocProvider 子树内
    children: [
      FloatingActionButton(
        onPressed: () => context.read<CounterBloc>().add(Increment()),
        ...
      ),
    ],
  ),
),

Builder 会在当前位置插入一个新的 BuildContext,这个 context 位于 BlocProvider 的子树中,向上查找时能命中注入的 CounterBloc。

3.4 修复方案二:把 Provider 提到路由层(更工程化)

如果页面结构复杂,到处包 Builder 不优雅,可以直接在路由注册时注入:

// main.dart
BlocDemoPage.routeName: (_) => BlocProvider(
  create: (_) => CounterBloc(),
  child: const BlocDemoPage(),
),

这样 BlocDemoPage 内部就不需要再写 BlocProvider,整个页面的 context(包括 FAB)都在 Provider 之下,不会再有作用域问题。

两种方案选哪种?

  • Demo / 单页验证:方案一,改动小
  • 正式工程:方案二,Provider 注入点集中在路由层,便于管理和测试

四、Bloc 的心智模型总结

跑通这个 Demo 后,你应该记住这张图:

┌─────────┐   add(Event)   ┌──────────┐   emit(State)   ┌──────────┐
│   UI    │ ──────────────▶│   Bloc   │ ───────────────▶│  State   │
└─────────┘                └──────────┘                 └──────────┘
     ▲                                                         │
     │                    BlocBuilder / BlocListener           │
     └─────────────────────────────────────────────────────────┘
  • UI 永远不直接改状态,只发 Event
  • Bloc 是唯一的状态生产者,所有变更走 emit
  • State 是不可变的,每次都是新对象
  • UI 通过 BlocBuilder 响应式订阅,状态变了自动重建

这套模型的好处是:任何一个状态变化都能追溯到具体的 Event,调试时只需要看 Bloc 的事件流就能定位问题。对于复杂业务(比如电商下单、支付流程),这种可追溯性价值极高。


五、给 Riverpod 用户的对标速查

如果你和我一样主力用 Riverpod,这张对照表能帮你快速建立映射:

Bloc 概念Riverpod 对标
Event不存在对应物(直接调 notifier 方法)
StateStateProvider 的 state / Notifier 的 state
BlocNotifier / AsyncNotifier
BlocProviderProviderScope 内的 Provider
BlocBuilderref.watch(provider)
context.read().add(Event)ref.read(provider.notifier).method()
BlocListenerref.listen(provider, ...)

六、下一步可以怎么扩展?

这个计数器只是 Bloc 的"Hello World",真实场景中你会遇到:

  1. 异步状态:比如加载数据,用 emit 配合 isLoading 状态,或者用 Future + AsyncValue 思路
  2. 状态去重:给 State 加 Equatable,相同状态不触发重建
  3. 多事件组合:用 on<Event>.debounce() 做防抖(搜索输入框神器)
  4. Bloc 间通信:BlocListener 监听一个 Bloc,add 事件到另一个 Bloc
  5. 测试:bloc_test 包可以非常方便地写 "given event, expect state" 的单测

写在最后

Bloc 不是银弹,它的样板代码确实比 Riverpod 多。但它强约束的 Event → State 模型,在团队协作和复杂业务场景下能带来更好的可维护性和可调试性。

建议: 如果你正在选型,先用 Riverpod 快速迭代,遇到状态流转复杂、需要审计的模块时,再局部引入 Bloc。两者不冲突,关键是理解背后的"单向数据流"思想。

如果你在 Bloc 实践中也遇到过什么坑,欢迎评论区交流~


simulator_screenshot_E1E42A85-DE3C-474F-AC21-23D62D5BB143.png