Flutter BLoC 状态管理框架深入分析
文档说明与版本基线
本文中的示例代码采用 Flutter 空安全写法,适合 Flutter 3.x + Dart 3.x 项目阅读和实践。
文中使用的包版本基线如下:
yaml
dependencies:
flutter:
sdk: flutter
flutter_bloc: ^8.1.6
equatable: ^2.0.5
dev_dependencies:
flutter_test:
sdk: flutter
bloc_test: ^9.1.7
mocktail: ^1.0.4
这些版本分别对应:
flutter:本文未绑定具体小版本,但默认是Flutter 3.x与Dart 3.x生态。flutter_bloc: ^8.1.6:本文中的BlocProvider、BlocBuilder、BlocListener、context.read()等用法按这一代 API 写法说明。equatable: ^2.0.5:用于做状态值比较,减少手写==和hashCode。bloc_test: ^9.1.7:用于测试Cubit和Bloc的状态序列。mocktail: ^1.0.4:用于 mock 仓库层、接口层等外部依赖。
如果你的项目版本不同,通常只会影响少量 API 细节,不影响本文讲的整体思路。尤其要注意:
- 旧版
bloc常见mapEventToState - 新版
bloc更推荐on<Event>()
本文统一采用当前更主流的新版写法:
Cubit直接通过方法调用并emitBloc通过on<Event>()注册事件处理器- Flutter 层使用
flutter_bloc
1. BLoC 是什么,为什么要用
BLoC 全称是 Business Logic Component。它的目标不是单纯保存状态,而是把应用中的三类职责拆开:
- UI 负责展示
- 业务逻辑负责处理流程
- 状态负责描述结果
在 Flutter 里,大家通常说的 BLoC,主要就是指:
blocflutter_bloc
一句话概括它的工作方式:
UI 发起动作,
Bloc/Cubit处理逻辑并输出新状态,界面只根据状态重建。
在简单页面里,setState() 当然足够用;但在这些业务场景里,BLoC 往往更合适:
- 登录注册
- 表单校验
- 分页加载
- 搜索联想
- 购物车与订单流转
- 需要多人协作和可测试性的模块
它的优势主要在于:
- 单向数据流清晰
- UI 与业务解耦
- 更适合异步流程
- 更利于测试
- 更容易形成团队统一规范
2. 整体工作机制
先看最经典的数据流:
text
User Action -> Event/Method -> Bloc/Cubit -> State -> UI
如果是 Cubit,流程更简单:
text
用户操作 -> 调用 Cubit 方法 -> emit 新状态 -> UI 重建
如果是 Bloc,流程更标准:
text
用户操作 -> add(Event) -> Bloc 处理事件 -> emit 新状态 -> UI 重建
两者的本质区别:
Cubit没有事件层,适合简单状态Bloc有事件层,适合复杂流程
你可以把 BLoC 理解成一个受控状态机:
- 输入是方法调用或事件
- 中间是业务逻辑
- 输出是状态对象
- UI 只订阅状态并渲染
3. BLoC、Cubit、flutter_bloc 三者关系
Cubit
Cubit 是轻量版 BLoC。
特点:
- 不定义
Event - 直接通过方法触发状态变化
- 代码量更少
- 学习成本更低
适合场景:
- 计数器
- 开关切换
- Tab index
- 筛选条件
- 小型局部状态
Bloc
Bloc 是完整事件驱动模型。
特点:
- 输入是事件
- 输出是状态
- 流程表达更规范
- 更适合异步与复杂业务
适合场景:
- 登录流程
- 多字段表单
- 搜索与防抖
- 分页与刷新
- 订单流转
flutter_bloc
flutter_bloc 是 Flutter 接入层,负责把 Bloc/Cubit 接入 Widget 树。常见组件包括:
BlocProviderMultiBlocProviderRepositoryProviderBlocBuilderBlocListenerBlocConsumerBlocSelector
所以可以简单理解为:
bloc管状态流转flutter_bloc管 Flutter 侧的使用方式
4. 内部实现原理
理解原理后,很多 API 都会变得很自然。
4.1 Cubit 的本质
Cubit<S> 可以粗略理解成:
- 内部保存一个当前状态
state - 对外暴露一个
stream - 每次
emit(newState)时把状态广播出去
简化模型如下:
dart
abstract class SimpleCubit<S> {
S _state;
final _controller = StreamController<S>.broadcast();
SimpleCubit(this._state);
S get state => _state;
Stream<S> get stream => _controller.stream;
void emit(S newState) {
_state = newState;
_controller.add(newState);
}
Future<void> close() async {
await _controller.close();
}
}
真实实现会更复杂,但核心思想就是:
维护当前状态,并持续向订阅者广播变化。
4.2 Bloc 的本质
Bloc<Event, State> 可以理解成在 Cubit 的基础上增加了一条事件输入流。
简化模型:
dart
abstract class SimpleBloc<E, S> extends SimpleCubit<S> {
final _eventController = StreamController<E>();
SimpleBloc(super.initialState) {
_eventController.stream.listen((event) async {
await handleEvent(event);
});
}
void add(E event) {
_eventController.add(event);
}
Future<void> handleEvent(E event);
}
所以:
Cubit是"直接修改状态"Bloc是"先接收事件,再统一处理状态变化"
4.3 on<Event>() 是怎么工作的
现代 bloc 推荐写法是用 on<Event>() 注册处理器,而不是旧版 mapEventToState。
dart
on<LoginSubmitted>((event, emit) async {
emit(state.copyWith(isSubmitting: true));
try {
await repository.login(state.username, state.password);
emit(state.copyWith(isSubmitting: false, isSuccess: true));
} catch (e) {
emit(state.copyWith(isSubmitting: false, errorMessage: e.toString()));
}
});
其本质是:
- 为某类事件注册处理函数
- 事件进入
Bloc后匹配到对应处理器 - 处理器通过
emit输出新状态
4.4 为什么它适合异步场景
因为它把"动作"和"结果"拆开了。
比如搜索功能,动作可能有:
KeywordChangedSearchSubmittedSearchCanceled
而结果状态可能有:
initialloadingsuccessemptyfailure
这种拆分很适合:
- 请求中
- 请求成功
- 请求失败
- 多阶段流转
- 单元测试与问题回放
4.5 BlocObserver 的作用
它是全局观察器,可以统一监听:
Cubit的状态变化Bloc的状态过渡- 错误信息
dart
class AppBlocObserver extends BlocObserver {
@override
void onChange(BlocBase bloc, Change change) {
super.onChange(bloc, change);
print('${bloc.runtimeType} $change');
}
@override
void onTransition(Bloc bloc, Transition transition) {
super.onTransition(bloc, transition);
print('${bloc.runtimeType} $transition');
}
@override
void onError(BlocBase bloc, Object error, StackTrace stackTrace) {
super.onError(bloc, error, stackTrace);
print('${bloc.runtimeType} $error');
}
}
void main() {
Bloc.observer = AppBlocObserver();
runApp(const MyApp());
}
适合做:
- 调试
- 日志
- 埋点
- 问题排查
4.6 flutter_bloc 如何接入界面
BlocProvider 的作用,本质上是把 Bloc/Cubit 放进 Widget 树中,供后代组件读取:
dart
BlocProvider(
create: (_) => CounterCubit(),
child: const CounterPage(),
)
子组件可以通过:
context.read<T>()context.watch<T>()BlocProvider.of<T>(context)
读取实例。
而 BlocBuilder、BlocListener 等组件负责订阅状态变化,并完成 UI 更新或副作用处理。
5. 核心组成:Event、State、Bloc
一个完整的 BLoC 模块一般由三部分构成。
Event
事件表示"发生了什么"。
dart
abstract class LoginEvent {}
class LoginUsernameChanged extends LoginEvent {
final String username;
LoginUsernameChanged(this.username);
}
class LoginPasswordChanged extends LoginEvent {
final String password;
LoginPasswordChanged(this.password);
}
class LoginSubmitted extends LoginEvent {}
要点:
- Event 表示动作,不表示结果
- 它更像命令或输入
State
状态表示"当前处于什么情况"。
dart
class LoginState {
final String username;
final String password;
final bool isSubmitting;
final bool isSuccess;
final String? errorMessage;
const LoginState({
this.username = '',
this.password = '',
this.isSubmitting = false,
this.isSuccess = false,
this.errorMessage,
});
LoginState copyWith({
String? username,
String? password,
bool? isSubmitting,
bool? isSuccess,
String? errorMessage,
}) {
return LoginState(
username: username ?? this.username,
password: password ?? this.password,
isSubmitting: isSubmitting ?? this.isSubmitting,
isSuccess: isSuccess ?? this.isSuccess,
errorMessage: errorMessage,
);
}
}
推荐做法:
- 状态尽量不可变
- 统一使用
copyWith - 配合
equatable
Bloc / Cubit
Bloc/Cubit 负责:
- 接收输入
- 调用业务逻辑
- 发出新状态
一个最简单的 Cubit:
dart
class CounterCubit extends Cubit<int> {
CounterCubit() : super(0);
void increment() => emit(state + 1);
void decrement() => emit(state - 1);
}
6. 使用方式详解
这一部分通过典型场景把 BLoC 的用法讲清楚。
6.1 先用 Cubit 入门
依赖:
yaml
dependencies:
flutter:
sdk: flutter
flutter_bloc: ^8.1.6
定义 Cubit:
dart
import 'package:flutter_bloc/flutter_bloc.dart';
class CounterCubit extends Cubit<int> {
CounterCubit() : super(0);
void increment() => emit(state + 1);
void decrement() => emit(state - 1);
void reset() => emit(0);
}
注入到应用:
dart
void main() {
runApp(
BlocProvider(
create: (_) => CounterCubit(),
child: const MyApp(),
),
);
}
页面使用:
dart
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(title: const Text('Cubit Counter')),
body: Center(
child: BlocBuilder<CounterCubit, int>(
builder: (context, count) {
return Text('$count', style: const TextStyle(fontSize: 40));
},
),
),
floatingActionButton: Column(
mainAxisAlignment: MainAxisAlignment.end,
children: [
FloatingActionButton(
onPressed: () => context.read<CounterCubit>().increment(),
child: const Icon(Icons.add),
),
const SizedBox(height: 12),
FloatingActionButton(
onPressed: () => context.read<CounterCubit>().decrement(),
child: const Icon(Icons.remove),
),
],
),
),
);
}
}
这个例子说明了三件事:
BlocProvider负责创建和注入Cubitcontext.read()用于调用方法BlocBuilder用于订阅状态并重建 UI
6.2 Bloc 标准写法:登录示例
Event、State 定义见上文第 5 节。Bloc 核心实现:
dart
class LoginBloc extends Bloc<LoginEvent, LoginState> {
final AuthRepository authRepository;
LoginBloc(this.authRepository) : super(const LoginState()) {
on<LoginUsernameChanged>(_onUsernameChanged);
on<LoginPasswordChanged>(_onPasswordChanged);
on<LoginSubmitted>(_onSubmitted);
}
void _onUsernameChanged(LoginUsernameChanged event, Emitter<LoginState> emit) {
emit(state.copyWith(username: event.username, isValid: _validate(event.username, state.password)));
}
void _onPasswordChanged(LoginPasswordChanged event, Emitter<LoginState> emit) {
emit(state.copyWith(password: event.password, isValid: _validate(state.username, event.password)));
}
Future<void> _onSubmitted(LoginSubmitted event, Emitter<LoginState> emit) async {
if (!state.isValid || state.isSubmitting) return;
emit(state.copyWith(isSubmitting: true));
try {
await authRepository.login(username: state.username, password: state.password);
emit(state.copyWith(isSubmitting: false, isSuccess: true));
} catch (e) {
emit(state.copyWith(isSubmitting: false, errorMessage: e.toString()));
}
}
bool _validate(String username, String password) =>
username.isNotEmpty && password.length >= 6;
}
页面要点:BlocListener 处理成功/失败提示,BlocBuilder 根据 state.isValid、state.isSubmitting 控制按钮;输入框 onChanged 发 LoginUsernameChanged/LoginPasswordChanged,按钮发 LoginSubmitted。
6.3 分页列表思路
分页天然有多个状态阶段:首次加载、刷新、加载更多、失败、无更多数据。事件可设计为 ArticleFetched、ArticleRefreshed;状态包含 status、articles、hasReachedMax。Bloc 中 _onFetched 计算 nextPage 并追加数据,_onRefreshed 清空后拉第一页。页面在 initState 发 ArticleRefreshed,滚动到底部附近发 ArticleFetched。
7. UI 层常用组件和 API
BlocProvider
作用:创建并向下提供 Bloc/Cubit 实例。
dart
BlocProvider(
create: (_) => CounterCubit(),
child: const CounterPage(),
)
MultiBlocProvider
作用:同时提供多个 Bloc。
dart
MultiBlocProvider(
providers: [
BlocProvider(create: (_) => LoginBloc(authRepository)),
BlocProvider(create: (_) => ProfileCubit()),
],
child: const AppView(),
)
RepositoryProvider
作用:注入仓库、服务或数据源依赖。
dart
RepositoryProvider(
create: (_) => UserRepository(apiClient: ApiClient()),
child: const AppView(),
)
推荐数据流:
text
View -> Bloc/Cubit -> Repository -> API/DB
BlocBuilder
作用:根据状态重建 UI。
dart
BlocBuilder<CartBloc, CartState>(
builder: (context, state) {
return Text('商品数: ${state.items.length}');
},
)
BlocListener
作用:处理一次性副作用,不负责渲染。
dart
BlocListener<OrderBloc, OrderState>(
listener: (context, state) {
if (state is OrderSuccess) {
Navigator.of(context).pushNamed('/success');
}
},
child: const OrderPageBody(),
)
适合:
- 页面跳转
- Dialog
- SnackBar
- Toast
BlocConsumer
作用:在同一个位置同时处理"重建 UI"和"副作用"。
dart
BlocConsumer<LoginBloc, LoginState>(
listener: (context, state) {
if (state.isSuccess) {
Navigator.of(context).pushReplacementNamed('/home');
}
},
builder: (context, state) {
return Text(state.isSubmitting ? '登录中...' : '请登录');
},
)
BlocSelector
作用:只监听状态中的某一部分字段,减少重建。
dart
BlocSelector<LoginBloc, LoginState, bool>(
selector: (state) => state.isSubmitting,
builder: (context, isSubmitting) {
return isSubmitting
? const CircularProgressIndicator()
: const Text('Idle');
},
)
context.read()、context.watch()、context.select()
context.read<T>():
- 读取实例
- 不监听状态变化
- 常用于按钮点击时发事件或调用方法
dart
context.read<CounterCubit>().increment();
context.watch<T>():
- 读取并监听变化
- 当前 Widget 会在依赖变化时重建
context.select<T, R>():
- 只订阅你关心的字段
- 常用于优化重建范围
dart
final isSubmitting = context.select(
(LoginBloc bloc) => bloc.state.isSubmitting,
);
buildWhen 与 listenWhen
它们用于减少无效更新。
dart
BlocBuilder<LoginBloc, LoginState>(
buildWhen: (previous, current) =>
previous.isSubmitting != current.isSubmitting,
builder: (context, state) {
return Text(state.isSubmitting ? '提交中' : '空闲');
},
)
dart
BlocListener<LoginBloc, LoginState>(
listenWhen: (previous, current) =>
previous.errorMessage != current.errorMessage,
listener: (context, state) {
if (state.errorMessage != null) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(state.errorMessage!)),
);
}
},
child: const SizedBox(),
)
8. 状态设计、项目结构与最佳实践
8.1 状态怎么设计
状态设计常见有两种方式。
第一种是单一对象状态:
dart
class ProfileState {
final bool loading;
final User? user;
final String? error;
}
适合:
- 表单
- 列表
- 多字段组合状态页面
第二种是多子类状态:
dart
abstract class ProfileState {}
class ProfileInitial extends ProfileState {}
class ProfileLoading extends ProfileState {}
class ProfileLoaded extends ProfileState {
final User user;
ProfileLoaded(this.user);
}
class ProfileError extends ProfileState {
final String message;
ProfileError(this.message);
}
适合:
- 阶段边界非常清晰的流程
- 互斥状态明显的场景
经验上可以这样选:
- 表单和复杂组合状态:优先单一对象状态
- 加载中/成功/失败这类阶段型流程:优先多子类状态
8.2 推荐项目结构
中大型 Flutter 项目里,建议按 feature 拆分:
text
lib/
app/
app.dart
app_bloc_observer.dart
features/
login/
bloc/
login_bloc.dart
login_event.dart
login_state.dart
repository/
auth_repository.dart
view/
login_page.dart
article/
bloc/
article_bloc.dart
article_event.dart
article_state.dart
repository/
article_repository.dart
view/
article_page.dart
如果项目更大,可以继续拆成:
datadomainpresentation
8.3 推荐实践
- 页面只负责收集输入、发事件、渲染状态和处理副作用
Bloc/Cubit负责流程控制,不直接关心 UI 细节- 接口请求、数据库、缓存等逻辑放到
Repository - 状态尽量不可变,配合
copyWith和equatable - 不要做一个"全局超级 Bloc"管理所有功能
- 一个特性一个
Bloc/Cubit,按业务边界拆分 - 副作用放到
BlocListener,不要写进builder
9. 并发与事件处理
9.1 并发与事件顺序
实际项目中经常会遇到这些问题:
- 搜索输入过快,旧请求覆盖新请求
- 重复点击按钮导致多次提交
- 分页时多个加载更多请求同时发出
这时就会用到事件转换策略。比如:
dart
on<SearchKeywordChanged>(
_onKeywordChanged,
transformer: restartable(),
);
含义是:
- 新搜索到来时,取消旧搜索,只保留最后一次
再比如:
dart
on<LoginSubmitted>(
_onSubmitted,
transformer: droppable(),
);
含义是:
- 当前提交未完成时,后续重复提交直接丢弃
这类能力通常配合 bloc_concurrency 使用,在复杂异步场景中很重要。
10. 最小完整模板与结论
如果你想快速搭一个最小 BLoC 模板,可以按下面结构写。
状态:
dart
class DemoState {
final bool loading;
final String message;
const DemoState({
this.loading = false,
this.message = '',
});
DemoState copyWith({
bool? loading,
String? message,
}) {
return DemoState(
loading: loading ?? this.loading,
message: message ?? this.message,
);
}
}
事件:
dart
abstract class DemoEvent {}
class DemoStarted extends DemoEvent {}
Bloc:
dart
class DemoBloc extends Bloc<DemoEvent, DemoState> {
DemoBloc() : super(const DemoState()) {
on<DemoStarted>(_onStarted);
}
Future<void> _onStarted(
DemoStarted event,
Emitter<DemoState> emit,
) async {
emit(state.copyWith(loading: true));
await Future.delayed(const Duration(seconds: 1));
emit(state.copyWith(loading: false, message: 'Loaded'));
}
}
页面:
dart
class DemoPage extends StatelessWidget {
const DemoPage({super.key});
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (_) => DemoBloc()..add(DemoStarted()),
child: Scaffold(
appBar: AppBar(title: const Text('Demo')),
body: Center(
child: BlocBuilder<DemoBloc, DemoState>(
builder: (context, state) {
if (state.loading) {
return const CircularProgressIndicator();
}
return Text(state.message);
},
),
),
),
);
}
}
总结:
BLoC 本质上是一套"事件/方法驱动 -> 状态流转 -> UI 响应"的受控状态管理模型。
如果你的 Flutter 项目需要:
- 清晰的业务流程
- 更好的可维护性
- 更强的可测试性
- 团队统一的编码规范
那么 BLoC 依然是非常成熟且稳定的一种选择。