hydrated_bloc 10.1.1
hydrated_bloc: ^10.1.1 copied to clipboard
An extension to the bloc state management library which automatically persists and restores bloc states.
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