JHHK

欢迎来到我的个人网站
行者常至 为者常成

BLoC

目录

介绍

一、什么是 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)、需要完整事件审计追踪、同一方法在不同入口需要不同语义。


行者常至,为者常成!





R
Valine - A simple comment system based on Leancloud.