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

本文定位:最少代码实践 + 真实踩坑复盘。不讲源码原理,不堆概念,只带你用一个计数器 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)));
  }
}

这里有几个关键点:

  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:

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",真实场景中你会遇到:

  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 实践中也遇到过什么坑,欢迎评论区交流~


相关推荐
m0_738185822 小时前
Flutter 鸿蒙化实战:sqflite 适配 OpenHarmony,本地 SQLite 数据库
flutter·华为·harmonyos·鸿蒙
梦曦i4 小时前
uni-app、uni-app x 与 Flutter 深度对比:2026年跨平台开发框架选型指南
flutter·uni-app·uni-app x
恋猫de小郭4 小时前
Flitz ,一个把 Flutter 玩成热插拔 Preview 的有趣项目,不错的思路
android·前端·flutter
FungLeo4 小时前
成为全栈·Flutter App 篇·Refresh Token 旋转与并发 401:安全存储与会话代次
flutter·jwt·移动安全·身份认证·token刷新·成为全栈
m0_738185825 小时前
Flutter 鸿蒙化实战:simple_circular_progress_bar 适配 OpenHarmony,环形进度条
flutter·华为·harmonyos·鸿蒙
大龄秃头程序员21 小时前
Riverpod 深度解析:从响应式本质到最小可运行闭环
flutter
曹小强wxx1 天前
让 AI 写代码不“翻车”:用 Claude Code + Codex 给 Flutter 项目搭一套工程规范
flutter·ai编程·claude
西西学代码1 天前
像素和颜色之间的转换(Flutter)
前端·javascript·flutter