Skip to content
This repository was archived by the owner on Jul 9, 2020. It is now read-only.

Commit 20055fb

Browse files
committed
fix(hydrated_cubit): excessive fromJson invocations and storage reads
1 parent f890575 commit 20055fb

File tree

4 files changed

+131
-40
lines changed

4 files changed

+131
-40
lines changed

packages/hydrated_cubit/CHANGELOG.md

+5
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
# 0.1.3
2+
3+
- fix: excessive storage reads and `fromJson` invocations
4+
- docs: minor documentation improvements
5+
16
# 0.1.2
27

38
- fix: reintroduce migration code to ensure no data loss ([#67](https://github.com/felangel/hydrated_bloc/issues/67))

packages/hydrated_cubit/lib/src/hydrated_cubit.dart

+3-3
Original file line numberDiff line numberDiff line change
@@ -60,10 +60,10 @@ abstract class HydratedCubit<State> extends Cubit<State> {
6060
if (_state != null) return _state;
6161
try {
6262
final stateJson = storage.read(storageToken) as Map<dynamic, dynamic>;
63-
if (stateJson == null) return super.state;
64-
return fromJson(Map<String, dynamic>.from(stateJson));
63+
if (stateJson == null) return _state = super.state;
64+
return _state = fromJson(Map<String, dynamic>.from(stateJson));
6565
} on dynamic catch (_) {
66-
return super.state;
66+
return _state = super.state;
6767
}
6868
}
6969

packages/hydrated_cubit/pubspec.yaml

+4-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
name: hydrated_cubit
22
description: An extension to the cubit state management library which automatically persists and restores cubit states.
3-
version: 0.1.2
3+
version: 0.1.3
4+
repository: https://github.com/felangel/cubit
5+
issue_tracker: https://github.com/felangel/cubit/issues
46
homepage: https://github.com/felangel/cubit
7+
documentation: https://github.com/felangel/cubit
58

69
environment:
710
sdk: ">=2.7.0 <3.0.0"

packages/hydrated_cubit/test/hydrated_cubit_test.dart

+119-36
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import 'dart:async';
2+
import 'package:flutter/material.dart';
23
import 'package:flutter_test/flutter_test.dart';
34
import 'package:mockito/mockito.dart';
45
import 'package:hydrated_cubit/hydrated_cubit.dart';
@@ -7,11 +8,6 @@ import 'package:uuid/uuid.dart';
78

89
class MockStorage extends Mock implements Storage {}
910

10-
class MockCubit extends Mock implements HydratedCubit<dynamic> {
11-
@override
12-
String get storageToken => '${runtimeType.toString()}$id';
13-
}
14-
1511
class MyUuidHydratedCubit extends HydratedCubit<String> {
1612
MyUuidHydratedCubit() : super(Uuid().v4());
1713

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

21+
class MyCallbackHydratedCubit extends HydratedCubit<int> {
22+
MyCallbackHydratedCubit({this.onFromJsonCalled}) : super(0);
23+
24+
final ValueSetter<dynamic> onFromJsonCalled;
25+
26+
void increment() => emit(state + 1);
27+
28+
@override
29+
Map<String, int> toJson(int state) => {'value': state};
30+
31+
@override
32+
int fromJson(dynamic json) {
33+
onFromJsonCalled?.call(json);
34+
return json['value'] as int;
35+
}
36+
}
37+
2538
class MyHydratedCubit extends HydratedCubit<int> {
2639
MyHydratedCubit([this._id]) : super(0);
2740

@@ -64,13 +77,89 @@ void main() {
6477
HydratedCubit.storage = storage;
6578
});
6679

67-
group('SingleHydratedCubit', () {
68-
MyHydratedCubit cubit;
80+
test('reads from storage once upon initialization', () {
81+
MyCallbackHydratedCubit();
82+
verify<dynamic>(storage.read('MyCallbackHydratedCubit')).called(1);
83+
});
6984

70-
setUp(() {
71-
cubit = MyHydratedCubit();
72-
});
85+
test(
86+
'does not read from storage on subsequent state changes '
87+
'when cache value exists', () {
88+
when<dynamic>(storage.read('MyCallbackHydratedCubit')).thenReturn(
89+
{'value': 42},
90+
);
91+
final cubit = MyCallbackHydratedCubit();
92+
expect(cubit.state, 42);
93+
cubit.increment();
94+
expect(cubit.state, 43);
95+
verify<dynamic>(storage.read('MyCallbackHydratedCubit')).called(1);
96+
});
7397

98+
test(
99+
'does not deserialize state on subsequent state changes '
100+
'when cache value exists', () {
101+
final fromJsonCalls = <dynamic>[];
102+
when<dynamic>(storage.read('MyCallbackHydratedCubit')).thenReturn(
103+
{'value': 42},
104+
);
105+
final cubit = MyCallbackHydratedCubit(
106+
onFromJsonCalled: fromJsonCalls.add,
107+
);
108+
expect(cubit.state, 42);
109+
cubit.increment();
110+
expect(cubit.state, 43);
111+
expect(fromJsonCalls, [
112+
{'value': 42}
113+
]);
114+
});
115+
116+
test(
117+
'does not read from storage on subsequent state changes '
118+
'when cache is empty', () {
119+
when<dynamic>(storage.read('MyCallbackHydratedCubit')).thenReturn(null);
120+
final cubit = MyCallbackHydratedCubit();
121+
expect(cubit.state, 0);
122+
cubit.increment();
123+
expect(cubit.state, 1);
124+
verify<dynamic>(storage.read('MyCallbackHydratedCubit')).called(1);
125+
});
126+
127+
test('does not deserialize state when cache is empty', () {
128+
final fromJsonCalls = <dynamic>[];
129+
when<dynamic>(storage.read('MyCallbackHydratedCubit')).thenReturn(null);
130+
final cubit = MyCallbackHydratedCubit(
131+
onFromJsonCalled: fromJsonCalls.add,
132+
);
133+
expect(cubit.state, 0);
134+
cubit.increment();
135+
expect(cubit.state, 1);
136+
expect(fromJsonCalls, isEmpty);
137+
});
138+
139+
test(
140+
'does not read from storage on subsequent state changes '
141+
'when cache is malformed', () {
142+
when<dynamic>(storage.read('MyCallbackHydratedCubit')).thenReturn('{');
143+
final cubit = MyCallbackHydratedCubit();
144+
expect(cubit.state, 0);
145+
cubit.increment();
146+
expect(cubit.state, 1);
147+
verify<dynamic>(storage.read('MyCallbackHydratedCubit')).called(1);
148+
});
149+
150+
test('does not deserialize state when cache is malformed', () {
151+
final fromJsonCalls = <dynamic>[];
152+
when<dynamic>(storage.read('MyCallbackHydratedCubit')).thenReturn('{');
153+
final cubit = MyCallbackHydratedCubit(
154+
onFromJsonCalled: fromJsonCalls.add,
155+
);
156+
expect(cubit.state, 0);
157+
cubit.increment();
158+
expect(cubit.state, 1);
159+
expect(fromJsonCalls, isEmpty);
160+
});
161+
162+
group('SingleHydratedCubit', () {
74163
test('should throw HydratedStorageNotFound when storage is null', () {
75164
HydratedCubit.storage = null;
76165
expect(
@@ -93,7 +182,7 @@ void main() {
93182
test('should call storage.write when onTransition is called', () {
94183
final transition = const Transition(currentState: 0, nextState: 0);
95184
final expected = <String, int>{'value': 0};
96-
cubit.onTransition(transition);
185+
MyHydratedCubit().onTransition(transition);
97186
verify(storage.write('MyHydratedCubit', expected)).called(2);
98187
});
99188

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

119208
test('stores initial state when instantiated', () {
209+
MyHydratedCubit();
120210
verify<dynamic>(
121211
storage.write('MyHydratedCubit', {'value': 0}),
122212
).called(1);
123213
});
124214

125215
test('initial state should return 0 when fromJson returns null', () {
126216
when<dynamic>(storage.read('MyHydratedCubit')).thenReturn(null);
127-
expect(cubit.state, 0);
128-
verify<dynamic>(storage.read('MyHydratedCubit')).called(2);
217+
expect(MyHydratedCubit().state, 0);
218+
verify<dynamic>(storage.read('MyHydratedCubit')).called(1);
129219
});
130220

131221
test('initial state should return 0 when deserialization fails', () {
132222
when<dynamic>(storage.read('MyHydratedCubit'))
133223
.thenThrow(Exception('oops'));
134-
expect(cubit.state, 0);
224+
expect(MyHydratedCubit().state, 0);
135225
});
136226

137227
test('initial state should return 101 when fromJson returns 101', () {
138228
when<dynamic>(storage.read('MyHydratedCubit'))
139229
.thenReturn({'value': 101});
140-
expect(cubit.state, 101);
141-
verify<dynamic>(storage.read('MyHydratedCubit')).called(2);
230+
231+
expect(MyHydratedCubit().state, 101);
232+
verify<dynamic>(storage.read('MyHydratedCubit')).called(1);
142233
});
143234

144235
group('clear', () {
145236
test('calls delete on storage', () async {
146-
await cubit.clear();
237+
await MyHydratedCubit().clear();
147238
verify(storage.delete('MyHydratedCubit')).called(1);
148239
});
149240
});
150241
});
151242

152243
group('MultiHydratedCubit', () {
153-
MyMultiHydratedCubit multiCubitA;
154-
MyMultiHydratedCubit multiCubitB;
155-
156-
setUp(() {
157-
multiCubitA = MyMultiHydratedCubit('A');
158-
multiCubitB = MyMultiHydratedCubit('B');
159-
});
160-
161244
test('initial state should return 0 when fromJson returns null', () {
162245
when<dynamic>(storage.read('MyMultiHydratedCubitA')).thenReturn(null);
163-
expect(multiCubitA.state, 0);
164-
verify<dynamic>(storage.read('MyMultiHydratedCubitA')).called(2);
246+
expect(MyMultiHydratedCubit('A').state, 0);
247+
verify<dynamic>(storage.read('MyMultiHydratedCubitA')).called(1);
165248

166249
when<dynamic>(storage.read('MyMultiHydratedCubitB')).thenReturn(null);
167-
expect(multiCubitB.state, 0);
168-
verify<dynamic>(storage.read('MyMultiHydratedCubitB')).called(2);
250+
expect(MyMultiHydratedCubit('B').state, 0);
251+
verify<dynamic>(storage.read('MyMultiHydratedCubitB')).called(1);
169252
});
170253

171254
test('initial state should return 101/102 when fromJson returns 101/102',
172255
() {
173256
when<dynamic>(storage.read('MyMultiHydratedCubitA'))
174257
.thenReturn({'value': 101});
175-
expect(multiCubitA.state, 101);
176-
verify<dynamic>(storage.read('MyMultiHydratedCubitA')).called(2);
258+
expect(MyMultiHydratedCubit('A').state, 101);
259+
verify<dynamic>(storage.read('MyMultiHydratedCubitA')).called(1);
177260

178261
when<dynamic>(storage.read('MyMultiHydratedCubitB'))
179262
.thenReturn({'value': 102});
180-
expect(multiCubitB.state, 102);
181-
verify<dynamic>(storage.read('MyMultiHydratedCubitB')).called(2);
263+
expect(MyMultiHydratedCubit('B').state, 102);
264+
verify<dynamic>(storage.read('MyMultiHydratedCubitB')).called(1);
182265
});
183266

184267
group('clear', () {
185268
test('calls delete on storage', () async {
186-
await multiCubitA.clear();
269+
await MyMultiHydratedCubit('A').clear();
187270
verify(storage.delete('MyMultiHydratedCubitA')).called(1);
188271
verifyNever(storage.delete('MyMultiHydratedCubitB'));
189272

190-
await multiCubitB.clear();
273+
await MyMultiHydratedCubit('B').clear();
191274
verify(storage.delete('MyMultiHydratedCubitB')).called(1);
192275
});
193276
});

0 commit comments

Comments
 (0)