Skip to content

Commit

Permalink
fix(hydrated_cubit): excessive fromJson invocations and storage reads
Browse files Browse the repository at this point in the history
  • Loading branch information
felangel committed Jul 7, 2020
1 parent f890575 commit 20055fb
Show file tree
Hide file tree
Showing 4 changed files with 131 additions and 40 deletions.
5 changes: 5 additions & 0 deletions packages/hydrated_cubit/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
# 0.1.3

- fix: excessive storage reads and `fromJson` invocations
- docs: minor documentation improvements

# 0.1.2

- fix: reintroduce migration code to ensure no data loss ([#67](https://github.com/felangel/hydrated_bloc/issues/67))
Expand Down
6 changes: 3 additions & 3 deletions packages/hydrated_cubit/lib/src/hydrated_cubit.dart
Original file line number Diff line number Diff line change
Expand Up @@ -60,10 +60,10 @@ abstract class HydratedCubit<State> extends Cubit<State> {
if (_state != null) return _state;
try {
final stateJson = storage.read(storageToken) as Map<dynamic, dynamic>;
if (stateJson == null) return super.state;
return fromJson(Map<String, dynamic>.from(stateJson));
if (stateJson == null) return _state = super.state;
return _state = fromJson(Map<String, dynamic>.from(stateJson));
} on dynamic catch (_) {
return super.state;
return _state = super.state;
}
}

Expand Down
5 changes: 4 additions & 1 deletion packages/hydrated_cubit/pubspec.yaml
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
name: hydrated_cubit
description: An extension to the cubit state management library which automatically persists and restores cubit states.
version: 0.1.2
version: 0.1.3
repository: https://github.com/felangel/cubit
issue_tracker: https://github.com/felangel/cubit/issues
homepage: https://github.com/felangel/cubit
documentation: https://github.com/felangel/cubit

environment:
sdk: ">=2.7.0 <3.0.0"
Expand Down
155 changes: 119 additions & 36 deletions packages/hydrated_cubit/test/hydrated_cubit_test.dart
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/mockito.dart';
import 'package:hydrated_cubit/hydrated_cubit.dart';
Expand All @@ -7,11 +8,6 @@ import 'package:uuid/uuid.dart';

class MockStorage extends Mock implements Storage {}

class MockCubit extends Mock implements HydratedCubit<dynamic> {
@override
String get storageToken => '${runtimeType.toString()}$id';
}

class MyUuidHydratedCubit extends HydratedCubit<String> {
MyUuidHydratedCubit() : super(Uuid().v4());

Expand All @@ -22,6 +18,23 @@ class MyUuidHydratedCubit extends HydratedCubit<String> {
String fromJson(dynamic json) => json['value'] as String;
}

class MyCallbackHydratedCubit extends HydratedCubit<int> {
MyCallbackHydratedCubit({this.onFromJsonCalled}) : super(0);

final ValueSetter<dynamic> onFromJsonCalled;

void increment() => emit(state + 1);

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

@override
int fromJson(dynamic json) {
onFromJsonCalled?.call(json);
return json['value'] as int;
}
}

class MyHydratedCubit extends HydratedCubit<int> {
MyHydratedCubit([this._id]) : super(0);

Expand Down Expand Up @@ -64,13 +77,89 @@ void main() {
HydratedCubit.storage = storage;
});

group('SingleHydratedCubit', () {
MyHydratedCubit cubit;
test('reads from storage once upon initialization', () {
MyCallbackHydratedCubit();
verify<dynamic>(storage.read('MyCallbackHydratedCubit')).called(1);
});

setUp(() {
cubit = MyHydratedCubit();
});
test(
'does not read from storage on subsequent state changes '
'when cache value exists', () {
when<dynamic>(storage.read('MyCallbackHydratedCubit')).thenReturn(
{'value': 42},
);
final cubit = MyCallbackHydratedCubit();
expect(cubit.state, 42);
cubit.increment();
expect(cubit.state, 43);
verify<dynamic>(storage.read('MyCallbackHydratedCubit')).called(1);
});

test(
'does not deserialize state on subsequent state changes '
'when cache value exists', () {
final fromJsonCalls = <dynamic>[];
when<dynamic>(storage.read('MyCallbackHydratedCubit')).thenReturn(
{'value': 42},
);
final cubit = MyCallbackHydratedCubit(
onFromJsonCalled: fromJsonCalls.add,
);
expect(cubit.state, 42);
cubit.increment();
expect(cubit.state, 43);
expect(fromJsonCalls, [
{'value': 42}
]);
});

test(
'does not read from storage on subsequent state changes '
'when cache is empty', () {
when<dynamic>(storage.read('MyCallbackHydratedCubit')).thenReturn(null);
final cubit = MyCallbackHydratedCubit();
expect(cubit.state, 0);
cubit.increment();
expect(cubit.state, 1);
verify<dynamic>(storage.read('MyCallbackHydratedCubit')).called(1);
});

test('does not deserialize state when cache is empty', () {
final fromJsonCalls = <dynamic>[];
when<dynamic>(storage.read('MyCallbackHydratedCubit')).thenReturn(null);
final cubit = MyCallbackHydratedCubit(
onFromJsonCalled: fromJsonCalls.add,
);
expect(cubit.state, 0);
cubit.increment();
expect(cubit.state, 1);
expect(fromJsonCalls, isEmpty);
});

test(
'does not read from storage on subsequent state changes '
'when cache is malformed', () {
when<dynamic>(storage.read('MyCallbackHydratedCubit')).thenReturn('{');
final cubit = MyCallbackHydratedCubit();
expect(cubit.state, 0);
cubit.increment();
expect(cubit.state, 1);
verify<dynamic>(storage.read('MyCallbackHydratedCubit')).called(1);
});

test('does not deserialize state when cache is malformed', () {
final fromJsonCalls = <dynamic>[];
when<dynamic>(storage.read('MyCallbackHydratedCubit')).thenReturn('{');
final cubit = MyCallbackHydratedCubit(
onFromJsonCalled: fromJsonCalls.add,
);
expect(cubit.state, 0);
cubit.increment();
expect(cubit.state, 1);
expect(fromJsonCalls, isEmpty);
});

group('SingleHydratedCubit', () {
test('should throw HydratedStorageNotFound when storage is null', () {
HydratedCubit.storage = null;
expect(
Expand All @@ -93,7 +182,7 @@ void main() {
test('should call storage.write when onTransition is called', () {
final transition = const Transition(currentState: 0, nextState: 0);
final expected = <String, int>{'value': 0};
cubit.onTransition(transition);
MyHydratedCubit().onTransition(transition);
verify(storage.write('MyHydratedCubit', expected)).called(2);
});

Expand All @@ -112,82 +201,76 @@ void main() {
final expectedError = Exception('oops');
final transition = const Transition(currentState: 0, nextState: 0);
when(storage.write(any, any)).thenThrow(expectedError);
cubit.onTransition(transition);
MyHydratedCubit().onTransition(transition);
}, onError: (dynamic _) => fail('should not throw'));
});

test('stores initial state when instantiated', () {
MyHydratedCubit();
verify<dynamic>(
storage.write('MyHydratedCubit', {'value': 0}),
).called(1);
});

test('initial state should return 0 when fromJson returns null', () {
when<dynamic>(storage.read('MyHydratedCubit')).thenReturn(null);
expect(cubit.state, 0);
verify<dynamic>(storage.read('MyHydratedCubit')).called(2);
expect(MyHydratedCubit().state, 0);
verify<dynamic>(storage.read('MyHydratedCubit')).called(1);
});

test('initial state should return 0 when deserialization fails', () {
when<dynamic>(storage.read('MyHydratedCubit'))
.thenThrow(Exception('oops'));
expect(cubit.state, 0);
expect(MyHydratedCubit().state, 0);
});

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

expect(MyHydratedCubit().state, 101);
verify<dynamic>(storage.read('MyHydratedCubit')).called(1);
});

group('clear', () {
test('calls delete on storage', () async {
await cubit.clear();
await MyHydratedCubit().clear();
verify(storage.delete('MyHydratedCubit')).called(1);
});
});
});

group('MultiHydratedCubit', () {
MyMultiHydratedCubit multiCubitA;
MyMultiHydratedCubit multiCubitB;

setUp(() {
multiCubitA = MyMultiHydratedCubit('A');
multiCubitB = MyMultiHydratedCubit('B');
});

test('initial state should return 0 when fromJson returns null', () {
when<dynamic>(storage.read('MyMultiHydratedCubitA')).thenReturn(null);
expect(multiCubitA.state, 0);
verify<dynamic>(storage.read('MyMultiHydratedCubitA')).called(2);
expect(MyMultiHydratedCubit('A').state, 0);
verify<dynamic>(storage.read('MyMultiHydratedCubitA')).called(1);

when<dynamic>(storage.read('MyMultiHydratedCubitB')).thenReturn(null);
expect(multiCubitB.state, 0);
verify<dynamic>(storage.read('MyMultiHydratedCubitB')).called(2);
expect(MyMultiHydratedCubit('B').state, 0);
verify<dynamic>(storage.read('MyMultiHydratedCubitB')).called(1);
});

test('initial state should return 101/102 when fromJson returns 101/102',
() {
when<dynamic>(storage.read('MyMultiHydratedCubitA'))
.thenReturn({'value': 101});
expect(multiCubitA.state, 101);
verify<dynamic>(storage.read('MyMultiHydratedCubitA')).called(2);
expect(MyMultiHydratedCubit('A').state, 101);
verify<dynamic>(storage.read('MyMultiHydratedCubitA')).called(1);

when<dynamic>(storage.read('MyMultiHydratedCubitB'))
.thenReturn({'value': 102});
expect(multiCubitB.state, 102);
verify<dynamic>(storage.read('MyMultiHydratedCubitB')).called(2);
expect(MyMultiHydratedCubit('B').state, 102);
verify<dynamic>(storage.read('MyMultiHydratedCubitB')).called(1);
});

group('clear', () {
test('calls delete on storage', () async {
await multiCubitA.clear();
await MyMultiHydratedCubit('A').clear();
verify(storage.delete('MyMultiHydratedCubitA')).called(1);
verifyNever(storage.delete('MyMultiHydratedCubitB'));

await multiCubitB.clear();
await MyMultiHydratedCubit('B').clear();
verify(storage.delete('MyMultiHydratedCubitB')).called(1);
});
});
Expand Down

0 comments on commit 20055fb

Please sign in to comment.