Skip to content

Commit

Permalink
feat(hydrated_bloc): allow overriding storage (#4314)
Browse files Browse the repository at this point in the history
  • Loading branch information
felangel authored Jan 4, 2025
1 parent 52f7eaf commit 324dfea
Show file tree
Hide file tree
Showing 2 changed files with 89 additions and 11 deletions.
23 changes: 12 additions & 11 deletions packages/hydrated_bloc/lib/src/hydrated_bloc.dart
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,8 @@ import 'package:meta/meta.dart';
abstract class HydratedBloc<Event, State> extends Bloc<Event, State>
with HydratedMixin {
/// {@macro hydrated_bloc}
HydratedBloc(State state) : super(state) {
hydrate();
HydratedBloc(State state, [Storage? storage]) : super(state) {
hydrate(storage);
}

static Storage? _storage;
Expand Down Expand Up @@ -75,8 +75,8 @@ abstract class HydratedBloc<Event, State> extends Bloc<Event, State>
abstract class HydratedCubit<State> extends Cubit<State>
with HydratedMixin<State> {
/// {@macro hydrated_cubit}
HydratedCubit(State state) : super(state) {
hydrate();
HydratedCubit(State state, [Storage? storage]) : super(state) {
hydrate(storage);
}
}

Expand Down Expand Up @@ -104,6 +104,8 @@ abstract class HydratedCubit<State> extends Cubit<State>
/// * [HydratedCubit] to enable automatic state persistence/restoration with [Cubit]
///
mixin HydratedMixin<State> on BlocBase<State> {
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.
Expand All @@ -116,10 +118,10 @@ mixin HydratedMixin<State> on BlocBase<State> {
/// ...
/// }
/// ```
void hydrate() {
final storage = HydratedBloc.storage;
void hydrate([Storage? storage]) {
__storage = storage ??= HydratedBloc.storage;
try {
final stateJson = storage.read(storageToken) as Map<dynamic, dynamic>?;
final stateJson = __storage.read(storageToken) as Map<dynamic, dynamic>?;
_state = stateJson != null ? _fromJson(stateJson) : super.state;
} catch (error, stackTrace) {
onError(error, stackTrace);
Expand All @@ -129,7 +131,7 @@ mixin HydratedMixin<State> on BlocBase<State> {
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);
Expand All @@ -145,12 +147,11 @@ mixin HydratedMixin<State> on BlocBase<State> {
@override
void onChange(Change<State> 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);
Expand Down Expand Up @@ -311,7 +312,7 @@ mixin HydratedMixin<State> on BlocBase<State> {
/// [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<void> clear() => HydratedBloc.storage.delete(storageToken);
Future<void> clear() => __storage.delete(storageToken);

/// Responsible for converting the `Map<String, dynamic>` representation
/// of the bloc state into a concrete instance of the bloc state.
Expand Down
77 changes: 77 additions & 0 deletions packages/hydrated_bloc/test/hydrated_bloc_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,18 @@ class MyUuidHydratedBloc extends HydratedBloc<String, String?> {
}
}

class MyHydratedBlocWithCustomStorage extends HydratedBloc<int, int> {
MyHydratedBlocWithCustomStorage(Storage storage) : super(0, storage);

@override
Map<String, int>? toJson(int state) {
return {'value': state};
}

@override
int? fromJson(Map<String, dynamic> json) => json['value'] as int?;
}

abstract class CounterEvent {}

class Increment extends CounterEvent {}
Expand Down Expand Up @@ -483,5 +495,70 @@ void main() {
);
});
});

group('MyHydratedBlocWithCustomStorage', () {
setUp(() {
HydratedBloc.storage = null;
});

test('should call storage.write when onChange is called', () {
const expected = <String, int>{'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<dynamic>()),
).thenThrow(expectedError);
bloc.onChange(change);
await Future<void>.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<dynamic>(() => storage.read(any())).thenReturn(null);
expect(MyHydratedBlocWithCustomStorage(storage).state, 0);
verify<dynamic>(
() => storage.read('MyHydratedBlocWithCustomStorage'),
).called(1);
});

test('initial state should return 101 when fromJson returns 101', () {
when<dynamic>(() => storage.read(any())).thenReturn({'value': 101});
expect(MyHydratedBlocWithCustomStorage(storage).state, 101);
verify<dynamic>(
() => storage.read('MyHydratedBlocWithCustomStorage'),
).called(1);
});

group('clear', () {
test('calls delete on custom storage', () async {
await MyHydratedBlocWithCustomStorage(storage).clear();
verify(
() => storage.delete('MyHydratedBlocWithCustomStorage'),
).called(1);
});
});
});
});
}

0 comments on commit 324dfea

Please sign in to comment.