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!
Our top sponsors are shown below! [Become a Sponsor]
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.
HydratedStorage
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
HydratedBloc.storage = await HydratedStorage.build(storageDirectory: ...);
runApp(App());
}
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 };
}
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.
class CounterCubit extends Cubit<int> with HydratedMixin {
CounterCubit() : super(0) {
hydrate();
}
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 };
}
Any storageDirectory
can be used when creating an instance of HydratedStorage
:
final storage = await HydratedStorage.build(
storageDirectory: await getApplicationDocumentsDirectory(),
);
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());
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());
// ...
});
Run this command:
With Dart:
$ dart pub add hydrated_bloc
With Flutter:
$ flutter pub add hydrated_bloc
This will add a line like this to your package's pubspec.yaml (and run an implicit dart pub get
):
dependencies:
hydrated_bloc: ^9.1.2
Alternatively, your editor might support dart pub get
or flutter pub get
. Check the docs for your editor to learn more.
Now in your Dart code, you can use:
import 'package:hydrated_bloc/hydrated_bloc.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:hydrated_bloc/hydrated_bloc.dart';
import 'package:path_provider/path_provider.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
HydratedBloc.storage = await HydratedStorage.build(
storageDirectory: kIsWeb
? HydratedStorage.webStorageDirectory
: await getTemporaryDirectory(),
);
runApp(const App());
}
class App extends StatelessWidget {
const App({super.key});
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (_) => BrightnessCubit(),
child: const AppView(),
);
}
}
class AppView extends StatelessWidget {
const AppView({super.key});
@override
Widget build(BuildContext context) {
return BlocBuilder<BrightnessCubit, Brightness>(
builder: (context, brightness) {
return MaterialApp(
theme: ThemeData(brightness: brightness),
home: const CounterPage(),
);
},
);
}
}
class CounterPage extends StatelessWidget {
const CounterPage({super.key});
@override
Widget build(BuildContext context) {
return BlocProvider<CounterBloc>(
create: (_) => CounterBloc(),
child: const CounterView(),
);
}
}
class CounterView extends StatelessWidget {
const CounterView({super.key});
@override
Widget build(BuildContext context) {
final textTheme = Theme.of(context).textTheme;
return Scaffold(
appBar: AppBar(title: const Text('Counter')),
body: Center(
child: BlocBuilder<CounterBloc, int>(
builder: (context, state) {
return Text('$state', style: textTheme.displayMedium);
},
),
),
floatingActionButton: Column(
crossAxisAlignment: CrossAxisAlignment.end,
mainAxisAlignment: MainAxisAlignment.end,
children: <Widget>[
FloatingActionButton(
child: const Icon(Icons.brightness_6),
onPressed: () => context.read<BrightnessCubit>().toggleBrightness(),
),
const SizedBox(height: 4),
FloatingActionButton(
child: const Icon(Icons.add),
onPressed: () {
context.read<CounterBloc>().add(CounterIncrementPressed());
},
),
const SizedBox(height: 4),
FloatingActionButton(
child: const Icon(Icons.remove),
onPressed: () {
context.read<CounterBloc>().add(CounterDecrementPressed());
},
),
const SizedBox(height: 4),
FloatingActionButton(
child: const Icon(Icons.delete_forever),
onPressed: () => HydratedBloc.storage.clear(),
),
],
),
);
}
}
sealed class CounterEvent {}
final class CounterIncrementPressed extends CounterEvent {}
final class CounterDecrementPressed extends CounterEvent {}
class CounterBloc extends HydratedBloc<CounterEvent, int> {
CounterBloc() : super(0) {
on<CounterIncrementPressed>((event, emit) => emit(state + 1));
on<CounterDecrementPressed>((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};
}
class BrightnessCubit extends HydratedCubit<Brightness> {
BrightnessCubit() : super(Brightness.light);
void toggleBrightness() {
emit(state == Brightness.light ? Brightness.dark : Brightness.light);
}
@override
Brightness fromJson(Map<String, dynamic> json) {
return Brightness.values[json['brightness'] as int];
}
@override
Map<String, dynamic> toJson(Brightness state) {
return <String, int>{'brightness': state.index};
}
}
Download details:
Author: bloclibrary.dev
Source: https://github.com/felangel/bloc/tree/master/packages/hydrated_bloc
#flutter #android #ios #web-development #dart