☰
鸿蒙Flutter复杂状态管理:BLoC模式与状态机设计实战
2026/10/14 7:30:26 网站建设 项目流程

从 Day 1 把 Flutter 环境跑在鸿蒙设备上,到前两天还在折腾平台通道和原生交互,说实话前四天都在解决“能不能跑”的问题。到了第五天,业务量和页面复杂度明显上来了,我不得不正面刚一个所有跨端开发迟早要面对的问题:复杂业务状态到底放哪、怎么管。这篇文章就完整记录我用 BLoC 模式重构一个多状态订单流程的核心过程,从为什么选它、状态机怎么设计,到 UI 层怎么接入、踩了哪些坑。如果你也在鸿蒙上用 Flutter 做中大型业务,或者你正处于“setState 还能对付、但总觉得哪里不对劲”的阶段,这篇应该能给你一套可以直接抄作业的流式状态管理思路。


1. 为什么我在鸿蒙项目里最终选了 BLoC,而不是 setState 或 Provider

1.1 先说说我最开始是怎么把状态写乱的

我手头这个模拟项目 X,是一个跨端内容发布与审核工具,在鸿蒙平板和手机上都要跑。业务流程不复杂:用户创建一条内容草稿,提交后经过异步审核,审核可能有通过、驳回、系统异常等结果,管理员还能中途撤回。听起来就四个状态,但实际写起来,页面一多就失控了。

一开始我用最朴素的方式:每个页面自己维护一坨 bool 和枚举,比如isSubmitting、isPassed、needRetry,跨页面传参靠构造函数,页面间同步状态靠回调。结果就是:内容列表页要知道详情页有没有提交成功,详情页又要在回调里触发列表刷新,提交失败还要弹 SnackBar,管理端还要能打断当前状态。几天下来,光排查“为什么列表状态和详情状态不一致”就花了我大半天。

如果你也经历过这种状态一多就四处救火的情况,大概率能理解我当时的感受:问题不是某个页面逻辑写错了,而是状态没有一个统一的归属地,每个页面都在偷偷改自己的局部变量,改动一多,就没有一个清晰的“当前全局状态”可以依赖。

1.2 三套方案实测下来的真实差异

我在这个项目里实际对比过三种主流状态管理方案,各自都有明显适用场景:

  • setState:适合组件内部、状态变化非常低频的小场景。比如一个展开收起按钮,用 setState 没任何问题。但只要是跨页面、跨组件、有异步时序的状态,它就会暴露出状态同步成本高、新手容易乱调的缺点。
  • Provider + ChangeNotifier:比 setState 好很多,依赖注入也方便,但它本质上仍然是命令式的状态变更。你在业务逻辑里写notifyListeners(),UI 层来一个数据就刷新一个,逻辑复杂后,通知链路一长,不好回溯到底谁改了什么。
  • BLoC(Business Logic Component):核心是事件驱动 + 单向数据流。UI 只做一件事:向 BLoC 发送事件(Event),然后根据 BLoC 输出的状态(State)来渲染。状态怎么变、什么时候变,完全由 BLoC 内部决定,UI 不直接改数据。这种风格对“状态机”类业务格外友好,因为整个状态迁移路径可以集中在一个类里看清。

我最终选择 BLoC 还有一个现实原因:鸿蒙端和 Android/iOS 端共用同一套 Flutter 业务代码,BLoC 是纯 Dart 实现,不依赖任何 UI 上下文,天然适合跨端复用。后续即便鸿蒙原生侧要接入,BLoC 这一层逻辑也能原封不动地被测试和复用。

1.3 BLoC 帮我把三类“状态病”一次性治好了

接入了 BLoC 之后回头看,我原来那套 setState 写法其实患有三种典型状态病:

  • 状态散落病:每个页面各自定义状态变量,没有统一出口。BLoC 用“一个 BLoC 对应一类业务状态”的方式,把散落的变量收敛到一个类里。
  • 转移条件隐藏病:原来“能不能提交”“能不能取消”这类判断分散在按钮点击事件里,出了问题要靠人肉搜索。BLoC 通过状态转移守卫逻辑,把非法操作挡在入口处。
  • 非法状态漏网病:比如重复点击提交按钮导致的并发提交、审核完成后还能再取消等问题,原始写法很难防住。BLoC 的事件进入 BLoC 后按顺序处理,配合状态机守卫,这个问题可以干净利落地解决。

提示:如果你的业务状态只有两三个、互相之间没有复杂流转,用 BLoC 反而有点过度设计。先判断是否有“状态之间的转移规则比较复杂”这个前提,再决定要不要上这套方案。


2. 把“状态机”翻译成“事件流”的基本功

2.1 三个必懂概念:事件、状态、BLoC 本体

BLoC 模式的三件套,我习惯用外卖来类比。你在外卖 App 里点“提交订单”,这个点击动作就是事件(Event)。提交后页面显示“下单中”“已接单”“已送达”,这些都是状态(State)。而真正处理下单、接单、送达这套流程的中央调度系统,就是BLoC。

在一个 Flutter 应用里,这套映射是这样的:

  • Event:你定义的一个普通 Dart 类,描述用户意图或外部通知。比如SubmitOrderEvent、CancelOrderEvent。
  • State:也是普通 Dart 类,描述某个业务阶段下界面的所有数据。比如OrderSubmittingState、OrderSubmittedState。
  • BLoC:继承Bloc<Event, State>的类,你注册“当收到某个 Event 时执行什么逻辑”,然后通过emit把新状态推给 UI。

理解了这个关系,再看整套模式就顺了:UI 层只负责两件事,把用户动作包装成 Event 发出去,以及根据 State 渲染页面。它不直接改任何业务数据。

2.2 为什么每个状态都必须是新的对象

BLoC 模式里有一个很容易被忽略但极其重要的规则:状态是不可变的。每次状态变化,都必须是生成一个新的状态对象,而不是在原有对象上改字段。

我第一次用 BLoC 时犯过一个经典错误:写了一个可变类当 State,在 BLoC 里state.orderList.add(newItem),然后emit(state)。结果 UI 层根本没有刷新。原因就是BlocBuilder比较的是新旧状态对象,你 emit 的是同一个对象引用,它认为“没变化”,直接不重建 widget。

后来我改成不可变类的写法:用一个包含所有字段的 State 基类,每次变更时通过copyWith创建新实例,一切就正常了。这不仅是技术上的要求,也是思维方式的转变:UI 是状态的函数,你给我一个不可变的状态,我就能可预期地画出一种界面;你原地修改状态,UI 层根本感知不到变化,逻辑就不可追踪了。

2.3 流式思维到底“流”在哪里

很多人看 BLoC 文档,最难理解的点就是 Stream。我刚开始也纠结:我又不是在写数据管道,为什么要跟 Stream 打交道?

换个角度来看这件事。传统写法里,代码是“拉”数据:页面要展示列表时,自己去调一个方法拿返回值。而 BLoC 是“推”数据:页面创建时订阅一个状态流,之后 BLoC 一旦 emit 新状态,页面就会自动收到通知,从而决定是否重建。

这就是“流式思维”:你不再关心数据什么时候来、怎么来,你只关心事件发出去之后,状态流会把你推到哪个状态。UI 就像坐在河边看风景的人,河水(状态流)流到哪一幕,他就画哪一幕。这个思路和状态机的契合度也极高:状态机关心的就是“当前状态 + 触发事件 → 新状态”,BLoC 内部天然就是这个结构。


3. 实战拆解:一个跨端内容审核状态机的完整实现

3.1 业务场景与状态转移表

我在模拟项目 X 里抽出来的业务场景是这样的:用户提交一条内容,系统先做本地格式校验,然后进入异步审核队列,审核可能通过、不通过,也可能因为服务异常进入失败态。管理员可以在任意非终态时撤回该内容。我把状态机定义成下面这张表:

当前状态触发事件允许?下一状态
草稿(Draft)提交(SubmitRequested)是提交中(Submitting)
提交中(Submitting)审核通过(AuditPassed)是通过(Passed)
提交中(Submitting)审核不通过(AuditRejected)是拒绝(Rejected)
提交中(Submitting)系统异常(AuditFailed)是失败(Failed)
提交中(Submitting)撤回(RevokeRequested)是撤回中(Revoking)
提交中(Submitting)再次提交(SubmitRequested)否保持不变
失败(Failed)重试(RetryRequested)是提交中(Submitting)
通过(Passed)撤回(RevokeRequested)是撤回中(Revoking)
撤回中(Revoking)撤回完成(RevokeFinished)是已撤回(Revoked)
已撤回(Revoked)任何事件否保持不变

这张表看起来很基础,但它就是整个状态机的地基。没有这张表,写代码时你会有无数个“万一”,有了这张表,每个事件在 BLoC 里是放行还是拦截,一查便知。

3.2 三步定义事件与状态类

定义状态与事件的方式非常直白,一共三步。第一步,定义状态抽象类和具体状态类。我在代码里给状态类加了一个description字段,方便调试时直接打印。

// 状态基类 abstract class AuditState { const AuditState({required this.description}); final String description; } // 草稿态 class AuditDraft extends AuditState { const AuditDraft() : super(description: '草稿'); } // 提交中 class AuditSubmitting extends AuditState { const AuditSubmitting() : super(description: '正在提交审核'); } // 通过 class AuditPassed extends AuditState { const AuditPassed({required this.contentId, required this.auditMessage}); final String contentId; final String auditMessage; @override String toString() => 'AuditPassed(contentId: $contentId, auditMessage: $auditMessage)'; } // 拒绝 class AuditRejected extends AuditState { const AuditRejected({required this.reason}); final String reason; } // 失败 class AuditFailed extends AuditState { const AuditFailed({required this.errorMessage}); final String errorMessage; } // 撤回中 class AuditRevoking extends AuditState { const AuditRevoking() : super(description: '正在撤回'); } // 已撤回 class AuditRevoked extends AuditState { const AuditRevoked({required this.revokeTime}); final DateTime revokeTime; }

第二步,定义事件类。注意事件类里可以携带参数,比如提交的内容、审核结果、错误信息,这些参数是 BLoC 做后续处理时能拿到的原料。

abstract class AuditEvent {} class DraftContentChanged extends AuditEvent { DraftContentChanged(this.content); final String content; } class SubmitRequested extends AuditEvent { SubmitRequested({this.contentId}); final String? contentId; } class AuditPassed extends AuditEvent { AuditPassed({required this.contentId, required this.auditMessage}); final String contentId; final String auditMessage; } class AuditRejected extends AuditEvent { AuditRejected({required this.reason}); final String reason; } class AuditFailed extends AuditEvent { AuditFailed({required this.errorMessage}); final String errorMessage; } class RetryRequested extends AuditEvent {} class RevokeRequested extends AuditEvent {}

第三步是用 flutter_bloc 包把 BLoC 类写出来。这里我用了两个关键 API:on<Event>注册事件处理器,emit(State)推送新状态。下面的代码里,我还特意给每个事件加了一个状态转移校验:先看当前状态允不允许触发这个事件,不允许就直接忽略并打印日志。

import 'package:flutter_bloc/flutter_bloc.dart'; class AuditBloc extends Bloc<AuditEvent, AuditState> { AuditBloc({required this.auditService}) : super(const AuditDraft()) { on<SubmitRequested>(_onSubmitRequested); on<AuditPassed>(_onAuditPassed); on<AuditRejected>(_onAuditRejected); on<AuditFailed>(_onAuditFailed); on<RetryRequested>(_onRetryRequested); on<RevokeRequested>(_onRevokeRequested); } final dynamic auditService; // 模拟服务层 Future<void> _onSubmitRequested(SubmitRequested event, Emitter<AuditState> emit) async { if (state is AuditSubmitting || state is AuditPassed || state is AuditRevoked) return; emit(const AuditSubmitting()); try { final contentId = await auditService.submit(event.contentId ?? 'new'); add(AuditPassed(contentId: contentId, auditMessage: '提交成功')); } catch (e) { add(AuditFailed(errorMessage: e.toString())); } } void _onAuditPassed(AuditPassed event, Emitter<AuditState> emit) { if (state is! AuditSubmitting) return; emit(AuditPassed(contentId: event.contentId, auditMessage: event.auditMessage)); } void _onAuditRejected(AuditRejected event, Emitter<AuditState> emit) { if (state is! AuditSubmitting) return; emit(AuditRejected(reason: event.reason)); } void _onAuditFailed(AuditFailed event, Emitter<AuditState> emit) { if (state is! AuditSubmitting) return; emit(AuditFailed(errorMessage: event.errorMessage)); } Future<void> _onRetryRequested(RetryRequested event, Emitter<AuditState> emit) async { if (state is! AuditFailed) return; emit(const AuditSubmitting()); try { final contentId = await auditService.submit('retry'); add(AuditPassed(contentId: contentId, auditMessage: '重试成功')); } catch (e) { add(AuditFailed(errorMessage: e.toString())); } } void _onRevokeRequested(RevokeRequested event, Emitter<AuditState> emit) { if (state is AuditDraft || state is AuditRevoked || state is AuditPassed == false) return; emit(const AuditRevoking()); // 模拟异步撤回 Future.delayed(const Duration(seconds: 1), () { if (!isClosed) emit(AuditRevoked(revokeTime: DateTime.now())); }); } }

这套实现有几个细节值得注意:

  • 事件处理器里第一步都是校验当前状态。比如_onAuditPassed里要求当前必须是AuditSubmitting,如果审核结果事件在错误的时机到达,直接忽略。这个设计就是状态机里的“非法转移拦截”。
  • add的用法值得留意。在异步逻辑里,我不直接在耗时操作后emit下一个状态,而是通过add(另一个事件)再次进入 BLoC 的事件处理流程。这种写法的好处是每个状态转移都有明确的事件记录,调 bug 时可以看到完整的事件链路。
  • isClosed是为了防止 BLoC 已经销毁后,延迟任务还在尝试 emit。做异步操作时这个判断非常重要。

3.3 UI 层接入:BlocProvider、BlocBuilder 与按钮状态

UI 层接入非常模板化。在页面顶层用BlocProvider提供 BLoC 实例,然后用BlocBuilder监听状态变化,不同状态渲染不同 UI。

class AuditPage extends StatelessWidget { @override Widget build(BuildContext context) { return BlocProvider( create: (_) => AuditBloc(auditService: AuditService()), child: Scaffold( appBar: AppBar(title: const Text('内容审核')), body: BlocBuilder<AuditBloc, AuditState>( builder: (context, state) { if (state is AuditDraft) { return _buildDraftView(context); } else if (state is AuditSubmitting) { return const Center(child: CircularProgressIndicator()); } else if (state is AuditPassed) { return _buildPassedView(state); } else if (state is AuditRejected) { return _buildRejectedView(state); } else if (state is AuditFailed) { return _buildFailedView(context, state); } else { return _buildRevokedView(state); } }, ), ), ); } }

每个 view 里,按钮通过context.read<AuditBloc>().add(SomeEvent())来发起事件。比如提交按钮:

ElevatedButton( onPressed: () => context.read<AuditBloc>().add(SubmitRequested()), child: const Text('提交审核'), )

到了这一步,你会发现 UI 层里几乎没有if (isXXX) {} else {}的业务逻辑判断,全部变成了“对状态做分支渲染”。这带来的直接好处就是UI 层做不了任何改变状态的动作,它只能请求 BLoC 去处理。

提示:不要直接context.read<AuditBloc>().state来判断页面逻辑。BlocBuilder 的 builder 里拿到的 state 才是当前应该渲染的依据;如果你直接读 state 再去改 UI,状态变化时不会触发 rebuild,很容易出现界面不更新的问题。


4. 状态机建模不踩坑:非法转移、事件并发和失败重试

4.1 把状态转移表变成运行时守卫

状态机建模最怕的不是“正常路径走不通”,而是“非法路径没拦住”。拿我之前的模拟业务来说,最典型的非法路径就是:用户连续点了两次提交按钮,第一次提交还没返回,第二次又发了一个SubmitRequested。

在传统 setState 写法里,你可能会用一个_isSubmitting布尔值来挡住,但这个布尔值只能挡一种情况,状态一多,你就要写一堆if (!_isSubmitting && !_isRevoking && ...),维护成本很高。BLoC 的写法就优雅很多:把允许的状态转移定义成一张映射表,事件进来先查表,查不到就拒绝。

class AuditStateMachine { static const Map<Type, List<Type>> _transitions = { AuditDraft: [AuditSubmitting], AuditSubmitting: [AuditPassed, AuditRejected, AuditFailed, AuditRevoking], AuditFailed: [AuditSubmitting, AuditRevoked], AuditPassed: [AuditRevoking], AuditRevoking: [AuditRevoked], AuditRevoked: [], }; static bool canTransition(AuditState from, Type to) { return _transitions[from.runtimeType]?.contains(to) ?? false; } }

有了这张表,BLoC 里的每个事件处理器都可以先来做守卫。比如_onAuditPassed里,你可以把if (state is! AuditSubmitting) return;换成更通用化的写法:

void _onAuditPassed(AuditPassed event, Emitter<AuditState> emit) { if (!AuditStateMachine.canTransition(state, AuditPassed)) return; emit(AuditPassed(contentId: event.contentId, auditMessage: event.auditMessage)); }

这种做法的好处一眼就能看出来:以后加一个新状态,只需要维护表里的一行数据,不需要到大大小小的函数里去翻逻辑。

4.2 BLoC 的事件排队机制其实是你的并发保险

BLoC 在处理同一类型事件时是默认串行处理的,也就是说,如果你连续 add 了两个SubmitRequested,BLoC 会先处理完第一个,再处理第二个。但注意,它不会自动“拦截”第二个,它只是排队。

所以防重复提交,不能只依赖 BLoC 的排队机制,一定要配合状态守卫。当第一个SubmitRequested已经把状态变成AuditSubmitting之后,第二个SubmitRequested进入处理器时,canTransition发现AuditSubmitting不能转移到AuditSubmitting,就会直接忽略。

我实测下来这个组合拳的效果很理想:不管用户多快点击多少次提交,最终只会触发一次真正的网络提交。这种“状态机决定放行与否”的思路比任何加锁方案都干净,因为你不需要在 UI 层维护额外的互斥变量。

4.3 失败重试的两种设计姿势

审核失败后的重试逻辑,我见过两种设计。第一种是把“重试”当成一个独立事件,如上面代码里的RetryRequested。第二种是把“重试”当作一个新的SubmitRequested来处理,因为反正最终目的都是提交,沿用同一个事件处理器。

我建议用独立事件RetryRequested,原因有两点。一是语义清晰:失败重试和首次提交虽然底层动作一样,但用户意图和 UI 展示可能不同。重试时你可能想记录“这是第 N 次尝试”,独立事件给这些附加信息留了空间。二是状态机守卫更严谨:独立事件可以专门定义“只有 AuditFailed 状态才能触发 RetryRequested”,一旦有人在错误状态发了重试,日志里一眼就能看清。


5. 实战中遇到的 4 个经典问题与排查实录

5.1 状态更新了但 UI 不刷新,原来是对象相等性在作怪

这个问题我在 2.2 小节提过,但这里专门展开说一下。当时我写完 BLoC 后,遇到一个诡异现象:日志明明打出了AuditPassed状态被 emit 出来了,但页面的BlocBuilder就是不重建。

排查过程是这样的:我先是加了大量日志确认 UI 有没有走进 builder;发现 builder 没有重新执行之后,怀疑是BlocProvider作用域问题;最后才发现是我自定义的 State 类没有重写==和hashCode。BlocBuilder默认通过判断新旧 State 是否相等来决定是否重建,如果两个AuditPassed实例只有contentId不同,但==没重写,它们就被认为是同一个对象,于是 builder 被跳过。

解决办法是给所有 State 类加上equatable依赖,让它们通过字段比较判断相等性:

class AuditPassed extends Equatable { const AuditPassed({required this.contentId, required this.auditMessage}); final String contentId; final String auditMessage; @override List<Object?> get props => [contentId, auditMessage]; }

这是我的第一个建议:从项目一开始就给 State 类统一加上 equatable,或者统一重写==。别等遇到问题时再补,因为项目大了以后补这个的排查成本非常高。

5.2 事件被“吞”了:为什么 add 之后毫无反应

还有一次,我在一个子页面里context.read<AuditBloc>().add(SubmitRequested()),但页面毫无反应。折腾了半天发现是作用域问题:子页面所在的BuildContext根本不在BlocProvider的包裹范围内,read找不到对应的 BLoC 实例。

排查方法非常简单:在read之后立刻用context.watch<AuditBloc>().state看一眼当前状态。如果read都找不到 BLoC,会直接抛异常;如果没有抛异常但事件没生效,再检查事件类型是否被正确注册到了on<>里。

另外要注意,在StatelessWidget的 build 方法里直接read一个 BLoC 再add事件,是合法的,但如果你在 build 过程中调用add,会导致 BLoC 在处理事件时又触发 UI rebuild,形成死循环。正确的姿势是永远在事件回调里 add,比如按钮的onPressed。

5.3 页面销毁后异步结果才回来,emit 直接报错

这个坑我在做异步撤回逻辑时踩过。页面已经返回上一级,BLoC 也调用了close(),但异步任务里的Future.delayed回调还在执行,这时候如果去emit,就会抛异常。

flutter_bloc 在emit时其实有安全检查,但更稳妥的写法是异步回调里先判断isClosed:

Future.delayed(const Duration(seconds: 1), () { if (!isClosed) emit(AuditRevoked(revokeTime: DateTime.now())); });

如果你要做的是网络请求后的回调,也建议在add进后续事件之前判断一下isClosed。虽然add到已关闭的 BLoC 也会被自动忽略,但提前判断日志更干净,排查起来更顺畅。

5.4 用 BlocObserver 把整个状态流转过程变成可视化日志

排这些坑的过程中,我最大的收获是养成了一个习惯:全局挂一个 BlocObserver。flutter_bloc 提供了BlocObserver这个抽象类,你只需要继承它,重写onEvent、onTransition、onError三个方法,然后把所有 BLoC 的事件、状态变化、异常都打印出来:

class AppBlocObserver extends BlocObserver { @override void onEvent(Bloc bloc, Object? event) { super.onEvent(bloc, event); debugPrint('${bloc.runtimeType} → event: $event'); } @override void onTransition(Bloc bloc, Transition transition) { super.onTransition(bloc, transition); debugPrint('${bloc.runtimeType} → state: ${transition.currentState} -> ${transition.nextState}'); } @override void onError(Bloc bloc, Object error, StackTrace stackTrace) { super.onError(bloc, error, stackTrace); debugPrint('${bloc.runtimeType} → error: $error'); } }

全局观察器特别适合排查“状态怎么就到了这个值”这类问题。你只需要把日志打开,一整套事件流和状态转移过程就摆在眼前,比在业务代码里插桩干净得多。我后来的所有 Flutter 项目都引入了这个观察器,跨端调试时省了非常多时间。


6. 这套 BLoC 状态机还能怎么扩展

6.1 多个 BLoC 之间怎么协作

真实业务里,页面级 BLoC 之间经常会互相通知。比如内容提交成功后,除了AuditBloc要把状态变成AuditPassed,全局的NoticeBloc可能还要推一条未读消息,UserBloc可能还要更新用户的发布计数。

这时候千万别让一个 BLoC 直接持有另一个 BLoC 的实例去add。正确的姿势是让页面自己做编排:页面层同时监听两个 BLoC,在某个 BLoC 的状态变化回调里,主动给另一个 BLoC 发事件。比如在BlocListener<AuditBloc, AuditState>里监听AuditPassed,然后调用context.read<NoticeBloc>().add(NoticeAdded(...))。这样每个 BLoC 仍然保持独立,状态流转不会被跨逻辑污染。

6.2 结合持久化做状态恢复

状态机终态(比如已撤回、已通过)通常要持久化,但非终态在 App 崩溃后恢复也是常见需求。BLoC 的模式在这里也很有优势:你可以在初始化 BLoC 时读取持久化数据,构造初始状态。

class AuditBloc extends Bloc<AuditEvent, AuditState> { AuditBloc({required this.auditService, AuditState? initialState}) : super(initialState ?? const AuditDraft()); }

页面启动时从本地数据库取最近一次状态,如果是AuditSubmitting,那说明上次提交可能已经发出但没收到结果,直接进入“检测上次提交状态”的恢复流程,而不是让用户傻等。这个思路对跨端业务很有用,因为鸿蒙 App 的生命周期管理和 Android 不完全一样,恢复现场能力尽量放在业务层,比平台层灵活得多。

6.3 把业务逻辑抽出来做单元测试

BLoC 是纯 Dart 类,这一点在测试上的优势太大了。你不需要启动 UI,不需要模拟点击,直接构造事件序列,验证状态是否如预期流转即可。

test('提交成功流程', () async { final bloc = AuditBloc(auditService: FakeAuditService()); expect(bloc.state, isA<AuditDraft>()); bloc.add(SubmitRequested()); await expectLater( bloc.stream, emitsInOrder([ isA<AuditSubmitting>(), isA<AuditPassed>(), ]), ); });

注意这里要记得在测试结束后await bloc.close()。我最初写测试时经常忘记 close,导致测试进程挂起,排查了很久才发现是 BLoC 的 StreamController 没关。


这五天真刀真枪写下来,我最真实的感受是:BLoC 模式的最大价值不是“把代码改好看了”,而是逼着你在写代码之前先想清楚业务状态有哪些、能怎么流转、不能怎么流转。流程不清晰的时候,setState 能让你糊弄过去,而 BLoC 会在建模阶段就逼你把问题露出来。如果你也想把现有的 Flutter 项目往 BLoC 重构,我建议从一条最核心的、状态流转最多的业务流程入手,先把状态转移表画出来,再动手拆事件和状态。等这条链路跑顺了,你会发现自己已经用上了流式思维来思考问题,而不是再依赖那些散落各处的临时变量了。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询