
hydrated_bloc 持久化状态管理为 Bloc/Cubit 自动保存与恢复状态的完整指南【免费下载链接】blocA predictable state management library that helps implement the BLoC design pattern项目地址: https://gitcode.com/gh_mirrors/bl/bloc本指南以hydrated_bloc包bloc 生态中负责状态持久化的官方扩展为核心讲解如何让 Bloc 与 Cubit 的状态在应用热重启、进程被杀甚至 Web 刷新后依然保留。你将掌握HydratedStorage的初始化、HydratedBloc/HydratedCubit/HydratedMixin三种接入方式、Storage抽象接口的自定义实现、hydration 错误处理策略以及基于 mocktail 的单元测试写法并结合本仓库源码理解其底层持久化与序列化机制。概览Storage 抽象与开箱即用的 HydratedStoragehydrated_bloc是 package:bloc 的扩展用于自动持久化persist与恢复restoreBloc 和 Cubit 的状态。它对外导出一个Storage接口这意味着它可以与任何存储提供方协作开箱即用它自带一个名为HydratedStorage的实现。从源码看Storage接口只定义了 5 个方法见 hydrated_storage.dartabstract class Storage { /// Returns value for key dynamic read(String key); /// Persists key value pair Futurevoid write(String key, dynamic value); /// Deletes key value pair Futurevoid delete(String key); /// Clears all key value pairs from storage Futurevoid clear(); /// Close the storage instance which will free any allocated resources. /// A storage instance can no longer be used once it is closed. Futurevoid close(); }而HydratedStorage正是Storage的标准实现它构建在 hive 给出了基于 Hive 存储在不同 Bloc 数量1150 个 storageToken与不同状态体积4 Bytes4 MB下的表现汇总可见其设计目标即是在移动端本地存储场景下保持高效。第一步初始化 HydratedStorage在使用任何HydratedBloc或HydratedCubit之前必须先给全局静态属性HydratedBloc.storage赋值。典型做法是在main()中先初始化再调用runAppFuturevoid main() async { WidgetsFlutterBinding.ensureInitialized(); HydratedBloc.storage await HydratedStorage.build( storageDirectory: kIsWeb ? HydratedStorageDirectory.web : HydratedStorageDirectory((await getTemporaryDirectory()).path), ); runApp(App()); }要点说明HydratedStorageDirectory.web是一个哨兵值sentinel用于告诉build走 Web 存储路径底层使用 Hive 的 Web/IndexedDB 后端见 hydrated_storage.dart。非 Web 平台通过path_provider的getTemporaryDirectory()临时目录或getApplicationDocumentsDirectory()文档目录拿到目录路径后包装成HydratedStorageDirectory传入。HydratedStorage.build内部使用synchronized包提供的Lock串行化初始化过程避免并发构建产生竞态见 hydrated_storage.dart。注意如果访问HydratedBloc.storage时尚未初始化会抛出StorageNotFound异常其提示信息会明确引导你调用HydratedBloc.storage await HydratedStorage.build();见 hydrated_bloc.dart。底层细节与 Hive 的隔离与旧数据迁移HydratedStorage.build在实现上刻意不直接使用全局Hive而是实例化内部HiveImpl以避免与用户自己直接调用Hive.init造成目录/Box 冲突源码注释引用了 hivedb/hive 的 issue #336。每次构建都会打开名为hydrated_box的 Box见 hydrated_storage.dart。此外针对历史版本build在非 Web 平台还会执行一次自动迁移如果存储目录下存在旧的.hydrated_bloc.json文件会将其中的 JSON 缓存逐条写入新的 Hive Box随后删除该文件见 _migration_io.dart。仓库测试 hydrated_storage_test.dart 验证了该迁移逻辑读者可在升级既有项目时放心依赖此行为。创建 HydratedCubitHydratedCubit是Cubit的专用子类构造时即自动完成状态恢复。你需要实现两个钩子方法fromJson把缓存的 Map 还原为状态和toJson把状态序列化为 Map。class CounterCubit extends HydratedCubitint { CounterCubit() : super(0); void increment() emit(state 1); override int fromJson(MapString, dynamic json) json[value] as int; override MapString, int toJson(int state) { value: state }; }创建 HydratedBlocHydratedBloc是Bloc的专用子类用法与HydratedCubit对称sealed class CounterEvent {} final class CounterIncrementPressed extends CounterEvent {} class CounterBloc extends HydratedBlocCounterEvent, int { CounterBloc() : super(0) { onCounterIncrementPressed((event, emit) emit(state 1)); } override int fromJson(MapString, dynamic json) json[value] as int; override MapString, int toJson(int state) { value: state }; }完成上述接入后CounterCubit与CounterBloc就会自动持久化/恢复状态你可以递增计数、热重启hot restart、杀死应用再启动甚至刷新 Web 页面上一次的状态都会被保留下来。完整的可运行示例见 example/lib/main.dart其中CounterBloc与BrightnessCubit持久化主题亮度共同演示了两种接入方式示例介绍见 example/README.md。使用 HydratedMixin 手动接入如果你的类无法继承HydratedBloc/HydratedCubit例如已经继承了其他基类可以使用HydratedMixin。必须在构造体内手动调用hydrate()这一点与自动构造的HydratedBloc/HydratedCubit不同源码文档在 hydrated_bloc.dart 中特别强调。class CounterCubit extends Cubitint with HydratedMixin { CounterCubit() : super(0) { hydrate(); // You must always call hydrate when using HydratedMixin } void increment() emit(state 1); override int fromJson(MapString, dynamic json) json[value] as int; override MapString, int toJson(int state) { value: state }; }hydrate()的完整签名如下见 hydrated_bloc.dartvoid hydrate({ Storage? storage, OnHydrationError onError defaultOnHydrationError, })其内部流程是先从存储中读取该实例对应的缓存 JSON若存在则通过_fromJson恢复_state若读取或反序列化失败则回调onError并回退到构造时的初始状态。当 Mixin 不必要的时候官方推荐直接继承HydratedBloc与HydratedCubit。存储命名空间storagePrefix 与 id每个 hydrated 实例在存储中的键由storageToken决定其组成为$storagePrefix$id见 hydrated_bloc.dart。storagePrefix默认取runtimeType.toString()。由于runtimeType在发布版经过混淆obfuscation或压缩minification后可能发生变化一旦变化就会导致旧的缓存失效、状态丢失——这在 Web 应用中尤为常见代码频繁发布会改变压缩后的runtimeType。因此 README 强烈建议在生产环境覆盖storagePrefixclass CounterCubit extends HydratedCubitint { CounterCubit() : super(0); override String get storagePrefix CounterCubit; }id默认为空字符串用于在同一类型存在多个实例时区分彼此。若你故意创建同一HydratedBloc类型的多个实例必须覆盖id返回唯一标识以保证各自的缓存互不干扰见 hydrated_bloc.dart。仓库测试中的MyMultiHydratedCubit即演示了通过id区分多个实例的用法见 hydrated_cubit_test.dart。为单个实例覆盖 StorageHydratedBloc.storage是全局静态存储但你可以在构造HydratedBloc或HydratedCubit时传入自定义Storage实例实现按实例的存储覆盖例如某个敏感模块使用加密存储其他模块使用默认存储class CounterCubit extends HydratedCubitint { CounterCubit() : super(0, storage: EncryptedStorage()); void increment() emit(state 1); override int fromJson(MapString, dynamic json) json[value] as int; override MapString, int toJson(int state) { value: state }; }对应源码中HydratedBloc与HydratedCubit的构造函数都接受可选的Storage? storage与OnHydrationError onHydrationError参数见 hydrated_bloc.dart 与 hydrated_bloc.dart随后调用hydrate(storage: storage, onError: onHydrationError)。测试用例MyHydratedCubitWithCustomStorage也演示了这一传参方式见 hydrated_cubit_test.dart。处理 hydration 错误HydrationErrorBehaviorhydrate可选地接受一个onError回调用于响应 hydration 错误并自定义发生错误后的缓存行为。回调必须返回一个HydrationErrorBehavior枚举值class CounterBloc extends BlocCounterEvent, int with HydratedMixin { CounterBloc() : super(0) { hydrate( onError: (error, stackTrace) { // Do something in response to hydration errors. // Must return a HydrationErrorBehavior to specify whether subsequent // state changes should be persisted. return HydrationErrorBehavior.retain; // Retain the previous state. } ); } ... }HydrationErrorBehavior只有两个取值见 hydrated_bloc.dart取值行为overwrite覆盖缓存hydration 出错后后续新发出的状态仍会被持久化旧的缓存将被覆盖。这是默认行为由defaultOnHydrationError提供见 hydrated_bloc.dartretain保留缓存hydration 出错后后续新状态不再写入存储直到下一次 hydrate 成功旧的缓存得以保留源码中该策略作用于两处hydrate()结束时决定是否回写初始状态以及onChange中决定是否持久化每次新状态见 hydrated_bloc.dart 与 hydrated_bloc.dart。自定义存储目录与可选加密HydratedStorage.build支持任意storageDirectory并额外支持encryptionCipher参数实现数据加密final storage await HydratedStorage.build( storageDirectory: await getApplicationDocumentsDirectory(), );带 AES 加密的初始化方式使用crypto包生成 256 位密钥再构造HydratedAesCipherimport package:crypto/crypto.dart; import package:hydrated_bloc/hydrated_bloc.dart; const password hydration; final byteskey sha256.convert(utf8.encode(password)).bytes; final storage await HydratedStorage.build( storageDirectory: HydratedStorageDirectory.web, encryptionCipher: HydratedAesCipher(byteskey), );HydratedCipher是抽象加密接口HydratedAesCipher是默认实现采用AES256 CBC PKCS7 padding见 hydrated_cipher.dart。注意build的storageDirectory是必填参数Web 平台请传入HydratedStorageDirectory.web。自定义 Storage 实现如果默认的HydratedStorage不满足需求你可以直接实现Storage接口再通过HydratedBloc.storage全局注入或用构造函数按实例注入。// my_hydrated_storage.dart class MyHydratedStorage implements Storage { override dynamic read(String key) { // TODO: implement read } override Futurevoid write(String key, dynamic value) async { // TODO: implement write } override Futurevoid delete(String key) async { // TODO: implement delete } override Futurevoid clear() async { // TODO: implement clear } }// main.dart HydratedBloc.storage MyHydratedStorage(); runApp(MyApp());这样你就把持久化层无缝替换为任何后端如数据库、远程同步、加密容器等而上层fromJson/toJson的序列化逻辑完全不变。若要在应用中手动清空某个实例的缓存可调用HydratedMixin提供的clear()它仅删除缓存、不会改变当前内存状态见 hydrated_bloc.dart示例应用中的删除按钮即调用HydratedBloc.storage.clear()清空全部缓存见 example/lib/main.dart。序列化机制自动的 JSON 遍历与循环检测在toJson/fromJson之外HydratedMixin还内置了一套状态序列化遍历机制见 hydrated_bloc.dart值得一提写入侧会递归遍历状态对象基础类型num、bool、null、String且num必须有限值直接编码List、Map递归转换Map 的 key 统一toString()其余对象尝试调用其toJson()。遍历过程中会检测循环引用一旦发现循环引用即抛出HydratedCyclicError进而包装为HydratedUnsupportedError避免序列化死循环见 hydrated_bloc.dart。若toJson返回null则该状态不会被持久化见 hydrated_bloc.dart读取侧也会对缓存中的 Map/List 做类型化的递归清洗_traverseRead。单元测试最佳实践用 mocktail 桩掉 Storage为使用了HydratedBloc的代码编写单元测试时官方推荐使用package:mocktail来桩stubStorage实现避免测试依赖真实磁盘或 Hiveimport package:flutter_test/flutter_test.dart; import package:hydrated_bloc/hydrated_bloc.dart; import package:mocktail/mocktail.dart; class MockStorage extends Mock implements Storage {} void main() { late Storage storage; setUp(() { storage MockStorage(); when( () storage.write(any(), anydynamic()), ).thenAnswer((_) async {}); HydratedBloc.storage storage; }); // ... }也可以在单个测试中桩storage.read以返回缓存状态模拟已存在旧状态的场景testWidgets(..., (tester) async { whendynamic(() storage.read($MyBloc)).thenReturn(MyState().toJson()); // ... });注意storage.read的 key 就是该实例的storageToken默认等于runtimeType字符串例如$MyBloc。仓库测试 hydrated_storage_test.dart 还覆盖了高负载并发写入场景验证HydratedStorage在多实例反复写入读取时的一致性hive_interference_test.dart 则专门验证了hydrated_bloc与用户直接使用 Hive 互不干扰。平台与版本支持支持平台根据 pubspec.yaml包括 Android、iOS、Linux、macOS、Web、Windows。Dart SDK 约束sdk: 2.14.0 4.0.0README 注明 Dart 2 最低 2.14。核心依赖bloc ^9.0.0、hive_ce ^2.0.0、meta、synchronized见 pubspec.yaml。依赖管理上只需在pubspec.yaml加入hydrated_bloc依赖即可获得HydratedBloc、HydratedCubit、HydratedMixin、Storage、HydratedStorage、HydratedStorageDirectory与HydratedAesCipher等全部 API见 hydrated_bloc.dart 的导出清单。结语hydrated_bloc把本地持久化从样板代码变成声明式能力接入HydratedBloc/HydratedCubit或HydratedMixinhydrate()并实现fromJson/toJson即可获得跨重启的状态保留再通过Storage接口、storagePrefix/id命名空间、HydrationErrorBehavior错误策略与加密 Cipher几乎可以覆盖从计数器到生产级应用的各类持久化需求。若要继续深入可阅读 源码入口 理解 hydration 全流程或参考 完整示例 上手实践。【免费下载链接】blocA predictable state management library that helps implement the BLoC design pattern项目地址: https://gitcode.com/gh_mirrors/bl/bloc创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考