前端【免费下载链接】blocA predictable state management library that helps implement the BLoC design pattern项目地址https://gitcode.com/gh_mirrors/bl/bloc点击查看免费下载本指南围绕 bricks/replay_cubit 这一 Mason 代码生成砖块展开讲解如何在 Dart 项目中一键生成带撤销undo与重做redo能力的 ReplayCubit并深入剖析其basic/equatable/freezed三种生成风格与底层实现原理。读完本文你将能独立完成 Brick 安装、参数化生成、模板定制理解以及让生成的 Cubit 真正具备可回滚的状态历史。一、背景ReplayCubit 是什么replay_cubitBrick 是 bloc 状态管理库 官方提供的代码生成工具之一其用途正如 Brick 元数据所描述Generate a new ReplayCubit in Dart. Built for the bloc state management library.见 brick.yaml。ReplayCubit 是 bloc 生态中一个特殊的 Cubit 变体它在普通Cubit之上自动叠加了状态历史记录能力。在 packages/replay_bloc/lib/src/replay_cubit.dart 中可以看到它的定义abstract class ReplayCubitState extends CubitState with ReplayCubitMixinState { ReplayCubit(State state, {int? limit}) : super(state) { if (limit ! null) { this.limit limit; } } }它继承自Cubit混入ReplayCubitMixin并允许传入可选的limit参数来限制撤销历史的最大长度默认不设上限。内部由_ChangeStack见 packages/replay_bloc/lib/src/change_stack.dart维护每次emit的状态快照从而支持undo()回退到上一个状态redo()重做被撤销的变更clearHistory()清空全部撤销/重做历史canUndo/canRedo查询当前是否可撤销/可重做limit设置历史栈容量上限。典型使用方式如下引自 replay_cubit.dart 的文档注释final cubit CounterCubit(); cubit.increment(); print(cubit.state); // 1 cubit.undo(); print(cubit.state); // 0 cubit.redo(); print(cubit.state); // 1正因ReplayCubit在emit内部自动拦截并记录变更replay_cubit.dart 中覆写了emit开发者无需为撤销/重做编写任何额外逻辑。而replay_cubitBrick 的价值就是把这类 Cubit 的样板代码标准化、模板化用一条命令即可生成。二、环境准备与安装 Brick2.1 安装 Mason CLIreplay_cubit是基于 Mason 明确要求 Ensure you have the mason_cli installed常见安装方式是通过 Dart 全局激活dart pub global activate mason_cli安装完成后可执行mason --version验证是否可用。2.2 将 Brick 添加到项目Brick 可以从官方 Brick Hub 市场安装也可以直接使用本仓库内的本地副本。本仓库的 bricks/mason.yaml 已预先注册了包括replay_cubit在内的全部 Brickbricks: bloc: path: ./bloc cubit: path: ./cubit hydrated_bloc: path: ./hydrated_bloc hydrated_cubit: path: ./hydrated_cubit replay_bloc: path: ./replay_bloc replay_cubit: path: ./replay_cubit flutter_bloc_feature: path: ./flutter_bloc_feature因此在仓库的bricks/目录下运行mason get即可加载全部本地注册的 Brick也可以在任何项目中将本仓库的 bricks/replay_cubit 目录作为本地路径添加mason add replay_cubit --path /path/to/bricks/replay_cubit或从 Brick Hub 安装mason add replay_cubit三、核心用法mason make replay_cubitBrick 安装完成后使用mason make命令即可生成新的 ReplayCubit。replay_cubit/README.md 给出的标准命令为mason make replay_cubit --name counter --style basic该命令会读取 brick.yaml 中声明的变量定义按模板生成两个 Dart 文件。3.1 变量说明replay_cubitBrick 支持两个核心变量完整定义如下变量描述默认值类型nameCubit 类的名称counterstringstyle生成的 Cubit 风格basic可选basic、equatable、freezedenum对应 brick.yaml 中的原始声明vars: name: type: string description: The name of the cubit class. default: counter prompt: Please enter the cubit name. style: type: enum description: The style of cubit generated. default: basic prompt: What is the cubit style? values: - basic - equatable - freezed两点实践提示name自动做命名转换传入counter后模板会通过snakeCase()生成文件名counter_cubit.dart、counter_state.dart并通过pascalCase()生成类名CounterCubit/CounterState。即使传入myFeature之类的混合大小写名称也会被统一规范化。不传参数也能用所有变量均有默认值mason make replay_cubit会进入交互模式依次以 Please enter the cubit name. 和 What is the cubit style? 提示输入对应 brick.yaml 中的prompt字段使用--name/--style命令行参数则可跳过交互便于脚本化批量生成。3.2 style 参数如何决定模板style 是一个枚举变量但它本身并不直接出现在模板中。真正的分发逻辑在生成前的 Hook 脚本 hooks/pre_gen.dart 里完成import package:mason/mason.dart; Futurevoid run(HookContext context) async { final style context.vars[style]; context.vars { ...context.vars, use_basic: style basic, use_equatable: style equatable, use_freezed: style freezed, }; }Hook 将style翻译成use_basic/use_equatable/use_freezed三个布尔标志写入变量上下文主模板再通过条件判断{{name.snakeCase()}}_cubit.dart}}_cubit.dart)选取对应的局部模板partial{{#use_freezed}}{{ freezed_cubit }}{{/use_freezed}}{{#use_equatable}}{{ equatable_cubit }}{{/use_equatable}}{{#use_basic}}{{ basic_cubit }}{{/use_basic}}状态文件{{name.snakeCase()}}_state.dart}}_state.dart)也采用完全相同的分发逻辑。这套enum → Hook 转换布尔标志 → 条件 partial的模式是 Mason 处理多风格模板的典型范式。四、三种生成风格详解无论选择哪种风格生成的 Cubit 都继承ReplayCubit区别仅在于 State 类如何实现相等性比较与序列化。4.1 basic零依赖最小实现选择--style basic时Cubit 模板{{~ basic_cubit }}为import package:replay_bloc/replay_bloc.dart; part {{name.snakeCase()}}_state.dart; class {{name.pascalCase()}}Cubit extends ReplayCubit{{name.pascalCase()}}State { {{name.pascalCase()}}Cubit() : super(const {{name.pascalCase()}}State()); }State 模板{{~ basic_state }}为part of {{name.snakeCase()}}_cubit.dart; class {{name.pascalCase()}}State { const {{name.pascalCase()}}State(); }要点State 通过part of与 Cubit 文件共享同一库因此 Cubit 侧只需import package:replay_bloc/replay_bloc.dart无任何第三方依赖构造函数用super(const CounterState())初始化初始状态State 为纯数据占位类未覆写/hashCode适合状态字段极少、无需做对象相等比较的最小场景。4.2 equatable基于值比较的状态--style equatable会引入equatable包。State 模板{{~ equatable_state }}为import package:equatable/equatable.dart; import package:replay_bloc/replay_bloc.dart; part {{name.snakeCase()}}_state.dart; class {{name.pascalCase()}}Cubit extends ReplayCubit{{name.pascalCase()}}State { {{name.pascalCase()}}Cubit() : super(const {{name.pascalCase()}}State()); }part of {{name.snakeCase()}}_cubit.dart; class {{name.pascalCase()}}State extends Equatable { const {{name.pascalCase()}}State(); override ListObject get props []; }要点CounterState extends Equatable并实现props后续为状态类添加字段时只需把字段加入props列表即获得基于字段值的与hashCode由于相等性基于值当emit一个与当前状态相等的状态时bloc 内部会跳过冗余的状态通知配合 ReplayCubit 的历史记录使用更加精准注意使用该风格时需要在项目pubspec.yaml中自行添加equatable依赖Brick 只生成源码不修改项目的依赖声明。4.3 freezed代码生成驱动的不可变状态--style freezed则采用freezed生态。模板{{~ freezed_cubit }}与{{~ freezed_state }}为import package:freezed_annotation/freezed_annotation.dart; import package:replay_bloc/replay_bloc.dart; part {{name.snakeCase()}}_state.dart; part {{name.snakeCase()}}_cubit.freezed.dart; class {{name.pascalCase()}}Cubit extends ReplayCubit{{name.pascalCase()}}State { {{name.pascalCase()}}Cubit() : super(const {{name.pascalCase()}}State.initial()); }part of {{name.snakeCase()}}_cubit.dart; freezed class {{name.pascalCase()}}State with _${{name.pascalCase()}}State { const factory {{name.pascalCase()}}State.initial() _Initial; }要点State 被freezed注解通过with _$CounterState混入生成的混合类得到一个不可变、带完整/hashCode/copyWith及模式匹配能力的状态类模板预置了CounterState.initial()工厂构造函数作为初始状态对应_Initial私有实现类由于part counter_cubit.freezed.dart引用了由 freezed 生成的文件使用该风格后必须在项目 pubspec.yaml 中添加freezed_annotation、freezed、build_runner依赖并运行构建命令dart run build_runner build生成.freezed.dart文件后代码方可编译运行。这是三种风格中配置成本最高、但表达能力最强的一种。五、生成输出结构与文件职责执行mason make replay_cubit --name counter --style basic后输出目录结构如下与 replay_cubit/README.md 一致├── counter_cubit.dart └── counter_state.dart两个文件的职责划分清晰文件内容职责counter_cubit.dartCounterCubit类extends ReplayCubitCounterState业务逻辑入口在此添加void increment() emit(...)等方法counter_state.dartCounterState类状态数据模型随所选风格决定相等性语义生成之后开发者通常会在 Cubit 类中补充业务方法。例如为计数器添加增减逻辑class CounterCubit extends ReplayCubitCounterState { CounterCubit() : super(const CounterState()); void increment() emit(state 1); void decrement() emit(state - 1); }每调用一次emit新的状态都会被自动压入撤销栈——撤销/重做能力是 ReplayCubit 内建的与生成的模板无关这正是该 Brick 与普通cubitBrick 的本质区别。六、驱动 ReplayCubit撤销、重做与历史管理生成代码只是起点理解ReplayCubit的运行时能力才能真正用好它。以下 API 均来自 packages/replay_bloc/lib/src/replay_cubit.dart 中ReplayCubitMixin的实现mixin ReplayCubitMixinState on CubitState { late final _changeStack _ChangeStackState(shouldReplay: shouldReplay); set limit(int limit) _changeStack.limit limit; override void emit(State state) { _changeStack.add( _ChangeState( this.state, state, () super.emit(state), (val) super.emit(val), ), ); super.emit(state); } void undo() _changeStack.undo(); void redo() _changeStack.redo(); bool get canUndo _changeStack.canUndo; bool get canRedo _changeStack.canRedo; void clearHistory() _changeStack.clear(); bool shouldReplay(State state) true; }6.1 核心行为emit自动记录历史每次emit都会向_ChangeStack追加一条_Change包含旧状态、新状态及两段恢复闭包随后正常发出新状态undo/redo从栈中回退或重放状态恢复过程同样通过super.emit通知监听者因此 UI 侧对状态流的订阅无需任何改动canUndo/canRedo用于在界面上动态禁用撤销/重做按钮避免越界操作clearHistory在表单提交成功、流程走完等场景主动销毁历史防止内存占用与误操作limit可在构造函数传入ReplayCubit(initial, limit: 50)历史超过 50 条时最旧的记录会被裁剪适用于状态频繁变更、需要控制内存的场景。6.2 测试佐证仓库的 packages/replay_bloc/test/replay_cubit_test.dart 提供了完整的撤销/重做行为测试包括limit生效、连续undo/redo、clear后历史清空等用例可以作为实现行为的可验证依据。在自己的项目中为生成的 Cubit 编写测试时建议至少覆盖emit后canUndo为真、undo后状态回退、redo后状态恢复、设置limit后历史被裁剪。七、与其他 Cubit 组合ReplayCubitMixin如果既要持久化HydratedCubit又要撤销/重做可以直接在自定义 Cubit 上混入ReplayCubitMixin而无需依赖生成的模板。该用法在 packages/replay_bloc/README.md 中有完整示例class CounterCubit extends HydratedCubitint with ReplayCubitMixin { CounterCubit() : super(0); void increment() emit(state 1); void decrement() emit(state - 1); override int fromJson(MapString, dynamic json) json[value] as int; override MapString, int toJson(int state) {value: state}; }由于ReplayCubitMixin是以on CubitState声明的 mixin它可以叠加在任何 Cubit 子类之上HydratedCubit、ReplayCubit本身等实现能力组合。同理ReplayBlocMixin也支持与HydratedBloc等组合使用参见 packages/replay_bloc/README.md 中的CounterBloc extends HydratedBlocCounterEvent, int with ReplayBlocMixin示例。八、扩展阅读Bloc Bricks 家族replay_cubit只是 Bloc 官方 Brick 集bricks/README.md中的一员。通过 bricks/mason.yaml 可以看到完整家族Brick用途bloc生成新的 Bloc含 Event/Statecubit生成新的 Cubithydrated_bloc生成支持持久化的 HydratedBlochydrated_cubit生成支持持久化的 HydratedCubitreplay_bloc生成支持撤销/重做的 ReplayBlocreplay_cubit生成支持撤销/重做的 ReplayCubit本文主题flutter_bloc_feature生成一个包含 Bloc 的完整 Flutter feature如果业务同时需要撤销/重做与持久化两类能力可参考replay_blocBrickbricks/replay_bloc与hydrated_*系列的模板结合上文第七节的 Mixin 组合方式按需定制。结语replay_cubit是一个小而精的代码生成砖块命令简单mason make replay_cubit --name counter --style basic、变量明确namestyle、三种风格覆盖从零依赖到 freezed 的渐进需求而其生成的ReplayCubit背后是_ChangeStack驱动的完整撤销/重做机制。将生成与机制两者结合理解你便能在项目中以最小成本获得可回滚、可重放、可清理的状态管理能力。赞分享前端【免费下载链接】blocA predictable state management library that helps implement the BLoC design pattern项目地址https://gitcode.com/gh_mirrors/bl/bloc点击查看免费下载相关推荐使用 hydrated_bloc Brick 快速生成可持久化的 HydratedBlocBloc 状态持久化使用 hydrated_bloc Brick 快速生成可持久化的 HydratedBlocBloc 状态持久化 HydratedBloc 是 bloc 状态前端Redux 撤销历史Undo/Redo实现指南从状态建模到高阶 Reducer 与 redux-undo 实战Redux 撤销历史Undo/Redo实现指南从状态建模到高阶 Reducer 与 redux undo 实战 导读 本文是 Redux 官方教程中的一篇前端Airi 前端实战用 VueUse useRefHistory 为 ref 构建可撤销undo/redo的变更历史Airi 前端实战用 VueUse useRefHistory 为 ref 构建可撤销undo/redo的变更历史 导读 useRefHistory 是AI 应用人工智能大模型数字人AI Agent语音前端后端桌面应用移动开发即时通讯3D渲染创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
