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