Skip to content

Commit 324dfea

Browse files
authored
feat(hydrated_bloc): allow overriding storage (#4314)
1 parent 52f7eaf commit 324dfea

File tree

2 files changed

+89
-11
lines changed

2 files changed

+89
-11
lines changed

packages/hydrated_bloc/lib/src/hydrated_bloc.dart

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,8 @@ import 'package:meta/meta.dart';
3333
abstract class HydratedBloc<Event, State> extends Bloc<Event, State>
3434
with HydratedMixin {
3535
/// {@macro hydrated_bloc}
36-
HydratedBloc(State state) : super(state) {
37-
hydrate();
36+
HydratedBloc(State state, [Storage? storage]) : super(state) {
37+
hydrate(storage);
3838
}
3939

4040
static Storage? _storage;
@@ -75,8 +75,8 @@ abstract class HydratedBloc<Event, State> extends Bloc<Event, State>
7575
abstract class HydratedCubit<State> extends Cubit<State>
7676
with HydratedMixin<State> {
7777
/// {@macro hydrated_cubit}
78-
HydratedCubit(State state) : super(state) {
79-
hydrate();
78+
HydratedCubit(State state, [Storage? storage]) : super(state) {
79+
hydrate(storage);
8080
}
8181
}
8282

@@ -104,6 +104,8 @@ abstract class HydratedCubit<State> extends Cubit<State>
104104
/// * [HydratedCubit] to enable automatic state persistence/restoration with [Cubit]
105105
///
106106
mixin HydratedMixin<State> on BlocBase<State> {
107+
late final Storage __storage;
108+
107109
/// Populates the internal state storage with the latest state.
108110
/// This should be called when using the [HydratedMixin]
109111
/// directly within the constructor body.
@@ -116,10 +118,10 @@ mixin HydratedMixin<State> on BlocBase<State> {
116118
/// ...
117119
/// }
118120
/// ```
119-
void hydrate() {
120-
final storage = HydratedBloc.storage;
121+
void hydrate([Storage? storage]) {
122+
__storage = storage ??= HydratedBloc.storage;
121123
try {
122-
final stateJson = storage.read(storageToken) as Map<dynamic, dynamic>?;
124+
final stateJson = __storage.read(storageToken) as Map<dynamic, dynamic>?;
123125
_state = stateJson != null ? _fromJson(stateJson) : super.state;
124126
} catch (error, stackTrace) {
125127
onError(error, stackTrace);
@@ -129,7 +131,7 @@ mixin HydratedMixin<State> on BlocBase<State> {
129131
try {
130132
final stateJson = _toJson(state);
131133
if (stateJson != null) {
132-
storage.write(storageToken, stateJson).then((_) {}, onError: onError);
134+
__storage.write(storageToken, stateJson).then((_) {}, onError: onError);
133135
}
134136
} catch (error, stackTrace) {
135137
onError(error, stackTrace);
@@ -145,12 +147,11 @@ mixin HydratedMixin<State> on BlocBase<State> {
145147
@override
146148
void onChange(Change<State> change) {
147149
super.onChange(change);
148-
final storage = HydratedBloc.storage;
149150
final state = change.nextState;
150151
try {
151152
final stateJson = _toJson(state);
152153
if (stateJson != null) {
153-
storage.write(storageToken, stateJson).then((_) {}, onError: onError);
154+
__storage.write(storageToken, stateJson).then((_) {}, onError: onError);
154155
}
155156
} catch (error, stackTrace) {
156157
onError(error, stackTrace);
@@ -311,7 +312,7 @@ mixin HydratedMixin<State> on BlocBase<State> {
311312
/// [clear] is used to wipe or invalidate the cache of a [HydratedBloc].
312313
/// Calling [clear] will delete the cached state of the bloc
313314
/// but will not modify the current state of the bloc.
314-
Future<void> clear() => HydratedBloc.storage.delete(storageToken);
315+
Future<void> clear() => __storage.delete(storageToken);
315316

316317
/// Responsible for converting the `Map<String, dynamic>` representation
317318
/// of the bloc state into a concrete instance of the bloc state.

packages/hydrated_bloc/test/hydrated_bloc_test.dart

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,18 @@ class MyUuidHydratedBloc extends HydratedBloc<String, String?> {
2727
}
2828
}
2929

30+
class MyHydratedBlocWithCustomStorage extends HydratedBloc<int, int> {
31+
MyHydratedBlocWithCustomStorage(Storage storage) : super(0, storage);
32+
33+
@override
34+
Map<String, int>? toJson(int state) {
35+
return {'value': state};
36+
}
37+
38+
@override
39+
int? fromJson(Map<String, dynamic> json) => json['value'] as int?;
40+
}
41+
3042
abstract class CounterEvent {}
3143

3244
class Increment extends CounterEvent {}
@@ -483,5 +495,70 @@ void main() {
483495
);
484496
});
485497
});
498+
499+
group('MyHydratedBlocWithCustomStorage', () {
500+
setUp(() {
501+
HydratedBloc.storage = null;
502+
});
503+
504+
test('should call storage.write when onChange is called', () {
505+
const expected = <String, int>{'value': 0};
506+
const change = Change(currentState: 0, nextState: 0);
507+
MyHydratedBlocWithCustomStorage(storage).onChange(change);
508+
verify(
509+
() => storage.write('MyHydratedBlocWithCustomStorage', expected),
510+
).called(2);
511+
});
512+
513+
test('should call onError when storage.write throws', () {
514+
runZonedGuarded(() async {
515+
final expectedError = Exception('oops');
516+
const change = Change(currentState: 0, nextState: 0);
517+
final bloc = MyHydratedBlocWithCustomStorage(storage);
518+
when(
519+
() => storage.write(any(), any<dynamic>()),
520+
).thenThrow(expectedError);
521+
bloc.onChange(change);
522+
await Future<void>.delayed(const Duration(milliseconds: 300));
523+
// ignore: invalid_use_of_protected_member
524+
verify(() => bloc.onError(expectedError, any())).called(2);
525+
}, (error, stackTrace) {
526+
expect(error.toString(), 'Exception: oops');
527+
expect(stackTrace, isNotNull);
528+
});
529+
});
530+
531+
test('stores initial state when instantiated', () {
532+
MyHydratedBlocWithCustomStorage(storage);
533+
verify(
534+
() => storage.write('MyHydratedBlocWithCustomStorage', {'value': 0}),
535+
).called(1);
536+
});
537+
538+
test('initial state should return 0 when fromJson returns null', () {
539+
when<dynamic>(() => storage.read(any())).thenReturn(null);
540+
expect(MyHydratedBlocWithCustomStorage(storage).state, 0);
541+
verify<dynamic>(
542+
() => storage.read('MyHydratedBlocWithCustomStorage'),
543+
).called(1);
544+
});
545+
546+
test('initial state should return 101 when fromJson returns 101', () {
547+
when<dynamic>(() => storage.read(any())).thenReturn({'value': 101});
548+
expect(MyHydratedBlocWithCustomStorage(storage).state, 101);
549+
verify<dynamic>(
550+
() => storage.read('MyHydratedBlocWithCustomStorage'),
551+
).called(1);
552+
});
553+
554+
group('clear', () {
555+
test('calls delete on custom storage', () async {
556+
await MyHydratedBlocWithCustomStorage(storage).clear();
557+
verify(
558+
() => storage.delete('MyHydratedBlocWithCustomStorage'),
559+
).called(1);
560+
});
561+
});
562+
});
486563
});
487564
}

0 commit comments

Comments
 (0)