An extension to package:bloc which automatically persists and restores bloc and cubit states. Built to work with package:bloc.
Learn more at bloclibrary.dev!
Sponsors
Our top sponsors are shown below! [Become a Sponsor]
|
|
|
Overview
hydrated_bloc exports a Storage interface which means it can work with any storage provider. Out of the box, it comes with its own implementation: HydratedStorage.
HydratedStorage is built on top of hive for a platform-agnostic, performant storage layer. See the complete example for more details.
Usage
Setup HydratedStorage
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
HydratedBloc.storage = await HydratedStorage.build(
storageDirectory: kIsWeb
? HydratedStorageDirectory.web
: HydratedStorageDirectory((await getTemporaryDirectory()).path),
);
runApp(App());
}
Create a HydratedCubit
class CounterCubit extends HydratedCubit<int> {
CounterCubit() : super(0);
void increment() => emit(state + 1);
@override
int fromJson(Map<String, dynamic> json) => json['value'] as int;
@override
Map<String, int> toJson(int state) => { 'value': state };
}
Create a HydratedBloc
sealed class CounterEvent {}
final class CounterIncrementPressed extends CounterEvent {}
class CounterBloc extends HydratedBloc<CounterEvent, int> {
CounterBloc() : super(0) {
on<CounterIncrementPressed>((event, emit) => emit(state + 1));
}
@override
int fromJson(Map<String, dynamic> json) => json['value'] as int;
@override
Map<String, int> toJson(int state) => { 'value': state };
}
Now the CounterCubit and CounterBloc will automatically persist/restore their state. We can increment the counter value, hot restart, kill the app, etc... and the previous state will be retained.
HydratedMixin
class CounterCubit extends Cubit<int> with HydratedMixin {
CounterCubit() : super(0) {
hydrate(); // You must always call `hydrate` when using `HydratedMixin`
}
void increment() => emit(state + 1);
@override
int fromJson(Map<String, dynamic> json) => json['value'] as int;
@override
Map<String, int> toJson(int state) => { 'value': state };
}
Storage Overrides
You can override the global storage instance for specific HydratedBloc or HydratedCubit instances by passing a custom storage instance via constructor.
class CounterCubit extends HydratedCubit<int> {
CounterCubit() : super(0, storage: EncryptedStorage());
void increment() => emit(state + 1);
@override
int fromJson(Map<String, dynamic> json) => json['value'] as int;
@override
Map<String, int> toJson(int state) => { 'value': state };
}
Handling Hydration Errors
You can optionally pass a custom onError callback to hydrate in order to handle any hydration errors and/or customize the caching behavior when a hydration error occurs.
class CounterBloc extends Bloc<CounterEvent, 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.
}
);
}
...
}
Custom Storage Directory
Any storageDirectory can be used when creating an instance of HydratedStorage:
final storage = await HydratedStorage.build(
storageDirectory: await getApplicationDocumentsDirectory(),
);
Custom Hydrated Storage
If the default HydratedStorage doesn't meet your needs, you can always implement a custom Storage by simply implementing the Storage interface and initializing HydratedBloc with the custom Storage.
// my_hydrated_storage.dart
class MyHydratedStorage implements Storage {
@override
dynamic read(String key) {
// TODO: implement read
}
@override
Future<void> write(String key, dynamic value) async {
// TODO: implement write
}
@override
Future<void> delete(String key) async {
// TODO: implement delete
}
@override
Future<void> clear() async {
// TODO: implement clear
}
}
// main.dart
HydratedBloc.storage = MyHydratedStorage();
runApp(MyApp());
Custom Storage Prefix
The storagePrefix defines the unique storage namespace for your HydratedBloc or HydratedCubit.
By default, it uses runtimeType, which isn't resilient to obfuscation or minification in production. If runtimeType changes, your saved state will be lost. This is especially relevant for web apps, where code changes frequently alter the minified runtimeType.
Consider overriding storagePrefix in production for more resilient, persistent storage.
class CounterCubit extends HydratedCubit<int> {
CounterCubit() : super(0);
@override
String get storagePrefix => 'CounterCubit';
}
Testing
When writing unit tests for code that uses HydratedBloc, it is recommended to stub the Storage implementation using package:mocktail.
import '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(), any<dynamic>()),
).thenAnswer((_) async {});
HydratedBloc.storage = storage;
});
// ...
}
You can also stub the storage.read API in individual tests to return cached state:
testWidgets('...', (tester) async {
when<dynamic>(() => storage.read('$MyBloc')).thenReturn(MyState().toJson());
// ...
});
Dart Versions
- Dart 2: >= 2.14
Maintainers
Libraries
- hydrated_bloc
- An extension to package:bloc which automatically persists and restores bloc and cubit states. Built to work with package:bloc.


