diff --git a/packages/hydrated_bloc/lib/src/hydrated_bloc.dart b/packages/hydrated_bloc/lib/src/hydrated_bloc.dart index 69dfe433eb4..dacbea9c316 100644 --- a/packages/hydrated_bloc/lib/src/hydrated_bloc.dart +++ b/packages/hydrated_bloc/lib/src/hydrated_bloc.dart @@ -33,8 +33,8 @@ import 'package:meta/meta.dart'; abstract class HydratedBloc extends Bloc with HydratedMixin { /// {@macro hydrated_bloc} - HydratedBloc(State state) : super(state) { - hydrate(); + HydratedBloc(State state, [Storage? storage]) : super(state) { + hydrate(storage); } static Storage? _storage; @@ -75,8 +75,8 @@ abstract class HydratedBloc extends Bloc abstract class HydratedCubit extends Cubit with HydratedMixin { /// {@macro hydrated_cubit} - HydratedCubit(State state) : super(state) { - hydrate(); + HydratedCubit(State state, [Storage? storage]) : super(state) { + hydrate(storage); } } @@ -104,6 +104,8 @@ abstract class HydratedCubit extends Cubit /// * [HydratedCubit] to enable automatic state persistence/restoration with [Cubit] /// mixin HydratedMixin on BlocBase { + late final Storage __storage; + /// Populates the internal state storage with the latest state. /// This should be called when using the [HydratedMixin] /// directly within the constructor body. @@ -116,10 +118,10 @@ mixin HydratedMixin on BlocBase { /// ... /// } /// ``` - void hydrate() { - final storage = HydratedBloc.storage; + void hydrate([Storage? storage]) { + __storage = storage ??= HydratedBloc.storage; try { - final stateJson = storage.read(storageToken) as Map?; + final stateJson = __storage.read(storageToken) as Map?; _state = stateJson != null ? _fromJson(stateJson) : super.state; } catch (error, stackTrace) { onError(error, stackTrace); @@ -129,7 +131,7 @@ mixin HydratedMixin on BlocBase { try { final stateJson = _toJson(state); if (stateJson != null) { - storage.write(storageToken, stateJson).then((_) {}, onError: onError); + __storage.write(storageToken, stateJson).then((_) {}, onError: onError); } } catch (error, stackTrace) { onError(error, stackTrace); @@ -145,12 +147,11 @@ mixin HydratedMixin on BlocBase { @override void onChange(Change change) { super.onChange(change); - final storage = HydratedBloc.storage; final state = change.nextState; try { final stateJson = _toJson(state); if (stateJson != null) { - storage.write(storageToken, stateJson).then((_) {}, onError: onError); + __storage.write(storageToken, stateJson).then((_) {}, onError: onError); } } catch (error, stackTrace) { onError(error, stackTrace); @@ -311,7 +312,7 @@ mixin HydratedMixin on BlocBase { /// [clear] is used to wipe or invalidate the cache of a [HydratedBloc]. /// Calling [clear] will delete the cached state of the bloc /// but will not modify the current state of the bloc. - Future clear() => HydratedBloc.storage.delete(storageToken); + Future clear() => __storage.delete(storageToken); /// Responsible for converting the `Map` representation /// of the bloc state into a concrete instance of the bloc state. diff --git a/packages/hydrated_bloc/test/hydrated_bloc_test.dart b/packages/hydrated_bloc/test/hydrated_bloc_test.dart index 252eda061b9..a45c4d105cb 100644 --- a/packages/hydrated_bloc/test/hydrated_bloc_test.dart +++ b/packages/hydrated_bloc/test/hydrated_bloc_test.dart @@ -27,6 +27,18 @@ class MyUuidHydratedBloc extends HydratedBloc { } } +class MyHydratedBlocWithCustomStorage extends HydratedBloc { + MyHydratedBlocWithCustomStorage(Storage storage) : super(0, storage); + + @override + Map? toJson(int state) { + return {'value': state}; + } + + @override + int? fromJson(Map json) => json['value'] as int?; +} + abstract class CounterEvent {} class Increment extends CounterEvent {} @@ -483,5 +495,70 @@ void main() { ); }); }); + + group('MyHydratedBlocWithCustomStorage', () { + setUp(() { + HydratedBloc.storage = null; + }); + + test('should call storage.write when onChange is called', () { + const expected = {'value': 0}; + const change = Change(currentState: 0, nextState: 0); + MyHydratedBlocWithCustomStorage(storage).onChange(change); + verify( + () => storage.write('MyHydratedBlocWithCustomStorage', expected), + ).called(2); + }); + + test('should call onError when storage.write throws', () { + runZonedGuarded(() async { + final expectedError = Exception('oops'); + const change = Change(currentState: 0, nextState: 0); + final bloc = MyHydratedBlocWithCustomStorage(storage); + when( + () => storage.write(any(), any()), + ).thenThrow(expectedError); + bloc.onChange(change); + await Future.delayed(const Duration(milliseconds: 300)); + // ignore: invalid_use_of_protected_member + verify(() => bloc.onError(expectedError, any())).called(2); + }, (error, stackTrace) { + expect(error.toString(), 'Exception: oops'); + expect(stackTrace, isNotNull); + }); + }); + + test('stores initial state when instantiated', () { + MyHydratedBlocWithCustomStorage(storage); + verify( + () => storage.write('MyHydratedBlocWithCustomStorage', {'value': 0}), + ).called(1); + }); + + test('initial state should return 0 when fromJson returns null', () { + when(() => storage.read(any())).thenReturn(null); + expect(MyHydratedBlocWithCustomStorage(storage).state, 0); + verify( + () => storage.read('MyHydratedBlocWithCustomStorage'), + ).called(1); + }); + + test('initial state should return 101 when fromJson returns 101', () { + when(() => storage.read(any())).thenReturn({'value': 101}); + expect(MyHydratedBlocWithCustomStorage(storage).state, 101); + verify( + () => storage.read('MyHydratedBlocWithCustomStorage'), + ).called(1); + }); + + group('clear', () { + test('calls delete on custom storage', () async { + await MyHydratedBlocWithCustomStorage(storage).clear(); + verify( + () => storage.delete('MyHydratedBlocWithCustomStorage'), + ).called(1); + }); + }); + }); }); }