Skip to content

Commit 6e7a203

Browse files
v2.0.1 - restore BehaviorSubject behavior for HydratedSubject (#25)
* Use BehaviorSubject internally * Clean up test messages, don't double-check listener behavior * Use expanded test reporter for CI (better logging) * Rename typing_utils.dart -> type_utils.dart * Add documentation note that the class behaves as `BehaviorSubject`. * Readability improvements * Remove unused completer, reformat * Fix value setter * Remove unused SubjectValueWrapper class * Update doc string * Update CHANGELOG.md, fix typo, bump version to 2.0.1
1 parent 70878bf commit 6e7a203

File tree

7 files changed

+115
-104
lines changed

7 files changed

+115
-104
lines changed

.github/workflows/flutter.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,4 +13,4 @@ jobs:
1313
- run: flutter pub get
1414
- run: flutter analyze
1515
- name: Run tests
16-
run: flutter test
16+
run: flutter test -r expanded

CHANGELOG.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
## 2.0.1
2+
3+
- Restore `BehaviorSubject` behavior of `HydratedSubject`.
4+
15
## 2.0.0
26

37
- Add null-safety support. Credits to @solid-yuriiprykhodko and @solid-vovabeloded.
@@ -14,7 +18,7 @@
1418
## 1.2.5
1519

1620
- Bump `rx_dart` version to `^0.22.0`
17-
- This breaks compotibility with Dart SDK < 2.6
21+
- This breaks compatibility with Dart SDK < 2.6
1822
- Simple CI added to repository
1923

2024
## 1.2.4

lib/src/hydrated.dart

Lines changed: 36 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,10 @@
11
import 'dart:async';
22

33
import 'package:flutter/foundation.dart';
4-
import 'package:hydrated/src/utils/typing_utils.dart';
4+
import 'package:hydrated/src/utils/type_utils.dart';
55
import 'package:rxdart/rxdart.dart';
66
import 'package:shared_preferences/shared_preferences.dart';
77

8-
import 'model/subject_value_wrapper.dart';
9-
108
/// A callback for encoding an instance of a data class into a String.
119
typedef PersistCallback<T> = String? Function(T);
1210

@@ -15,6 +13,8 @@ typedef HydrateCallback<T> = T Function(String);
1513

1614
/// A [Subject] that automatically persists its values and hydrates on creation.
1715
///
16+
/// Mimics the behavior of a [BehaviorSubject].
17+
///
1818
/// HydratedSubject supports serialized classes and [shared_preferences] types
1919
/// such as:
2020
/// - `int`
@@ -59,26 +59,21 @@ typedef HydrateCallback<T> = T Function(String);
5959
/// ```
6060
class HydratedSubject<T> extends Subject<T> implements ValueStream<T> {
6161
static final _areTypesEqual = TypeUtils.areTypesEqual;
62-
62+
final BehaviorSubject<T> _subject;
6363
final String _key;
6464
final HydrateCallback<T>? _hydrate;
6565
final PersistCallback<T>? _persist;
6666
final VoidCallback? _onHydrate;
6767
final T? _seedValue;
68-
final StreamController<T> _controller;
69-
70-
SubjectValueWrapper<T>? _wrapper;
7168

7269
HydratedSubject._(
7370
this._key,
7471
this._seedValue,
7572
this._hydrate,
7673
this._persist,
7774
this._onHydrate,
78-
this._controller,
79-
Stream<T> observable,
80-
this._wrapper,
81-
) : super(_controller, observable) {
75+
this._subject,
76+
) : super(_subject, _subject.stream) {
8277
_hydrateSubject();
8378
}
8479

@@ -107,28 +102,27 @@ class HydratedSubject<T> extends Subject<T> implements ValueStream<T> {
107102
(hydrate != null && persist != null));
108103

109104
// ignore: close_sinks
110-
final StreamController<T> controller = StreamController.broadcast(
111-
onListen: onListen,
112-
onCancel: onCancel,
113-
sync: sync,
114-
);
115-
116-
final wrapper =
117-
seedValue != null ? SubjectValueWrapper(value: seedValue) : null;
105+
final subject = seedValue != null
106+
? BehaviorSubject<T>.seeded(
107+
seedValue,
108+
onListen: onListen,
109+
onCancel: onCancel,
110+
sync: sync,
111+
)
112+
: BehaviorSubject<T>(
113+
onListen: onListen,
114+
onCancel: onCancel,
115+
sync: sync,
116+
);
118117

119118
return HydratedSubject._(
120-
key,
121-
seedValue,
122-
hydrate,
123-
persist,
124-
onHydrate,
125-
controller,
126-
Rx.defer(
127-
() => wrapper == null
128-
? controller.stream
129-
: controller.stream.startWith(wrapper.value!),
130-
reusable: true),
131-
wrapper);
119+
key,
120+
seedValue,
121+
hydrate,
122+
persist,
123+
onHydrate,
124+
subject,
125+
);
132126
}
133127

134128
/// A unique key that references a storage container
@@ -137,40 +131,37 @@ class HydratedSubject<T> extends Subject<T> implements ValueStream<T> {
137131

138132
@override
139133
void onAdd(T event) {
140-
_wrapper = SubjectValueWrapper(value: event);
134+
_subject.add(event);
141135
_persistValue(event);
142136
}
143137

144138
@override
145139
ValueStream<T> get stream => this;
146140

147141
@override
148-
bool get hasValue => _wrapper?.value != null;
142+
bool get hasValue => _subject.hasValue;
149143

150144
@override
151-
T? get valueOrNull => _wrapper?.value;
145+
T? get valueOrNull => _subject.valueOrNull;
152146

153147
/// Get the latest value emitted by the Subject
154148
@override
155-
T get value =>
156-
hasValue ? _wrapper!.value! : throw ValueStreamError.hasNoValue();
149+
T get value => _subject.value;
157150

158151
/// Set and emit the new value
159-
set value(T newValue) => add(newValue);
152+
set value(T newValue) => add(value);
160153

161154
@override
162-
Object get error => hasError
163-
? _wrapper!.errorAndStackTrace!
164-
: throw ValueStreamError.hasNoError();
155+
Object get error => _subject.error;
165156

166157
@override
167-
Object? get errorOrNull => _wrapper?.errorAndStackTrace;
158+
Object? get errorOrNull => _subject.errorOrNull;
168159

169160
@override
170-
bool get hasError => _wrapper?.errorAndStackTrace != null;
161+
bool get hasError => _subject.errorOrNull != null;
171162

172163
@override
173-
StackTrace? get stackTrace => _wrapper?.errorAndStackTrace?.stackTrace;
164+
StackTrace? get stackTrace => _subject.stackTrace;
174165

175166
/// Hydrates the HydratedSubject with a value stored on the user's device.
176167
///
@@ -204,8 +195,7 @@ class HydratedSubject<T> extends Subject<T> implements ValueStream<T> {
204195
// do not hydrate if the store is empty or matches the seed value
205196
// TODO: allow writing of seedValue if it is intentional
206197
if (val != null && val != _seedValue) {
207-
_wrapper = SubjectValueWrapper(value: val);
208-
_controller.add(val);
198+
_subject.add(val);
209199
}
210200

211201
_onHydrate?.call();
@@ -238,8 +228,7 @@ class HydratedSubject<T> extends Subject<T> implements ValueStream<T> {
238228
'HydratedSubject – value must be int, '
239229
'double, bool, String, or List<String>',
240230
);
241-
final errorAndTrace = ErrorAndStackTrace(error, StackTrace.current);
242-
_wrapper = SubjectValueWrapper(errorAndStackTrace: errorAndTrace);
231+
_subject.addError(error, StackTrace.current);
243232
}
244233
}
245234

lib/src/model/subject_value_wrapper.dart

Lines changed: 0 additions & 11 deletions
This file was deleted.

pubspec.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
name: hydrated
22
description: An automatically persisted BehaviorSubject with simple hydration for Flutter. Intended to be used with the BLoC pattern.
3-
version: 2.0.0
3+
version: 2.0.1
44
homepage: https://github.com/solid-software/hydrated
55

66
environment:

test/hydrated_test.dart

Lines changed: 72 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -6,73 +6,102 @@ import 'package:hydrated/hydrated.dart';
66
import 'package:shared_preferences/shared_preferences.dart';
77

88
void main() {
9-
SharedPreferences.setMockInitialValues({
10-
"flutter.prefs": true,
11-
"flutter.int": 1,
12-
"flutter.double": 1.1,
13-
"flutter.bool": true,
14-
"flutter.String": "first",
15-
"flutter.List<String>": ["a", "b"],
16-
"flutter.SerializedClass": '{"value":true,"count":42}'
9+
setUp(() {
10+
SharedPreferences.setMockInitialValues({
11+
"flutter.prefs": true,
12+
"flutter.int": 1,
13+
"flutter.double": 1.1,
14+
"flutter.bool": true,
15+
"flutter.String": "first",
16+
"flutter.List<String>": ["a", "b"],
17+
"flutter.SerializedClass": '{"value":true,"count":42}'
18+
});
1719
});
1820

19-
test('shared preferences', () async {
21+
test('Shared Preferences set mock initial values', () async {
2022
final prefs = await SharedPreferences.getInstance();
2123

2224
final value = prefs.getBool("prefs");
2325
expect(value, isTrue);
2426
});
2527

26-
test('int', () async {
27-
await testHydrated<int>("int", 1, 2);
28-
});
28+
group('HydratedSubject', () {
29+
group('correctly handles data type', () {
30+
test('int', () async {
31+
await testHydrated<int>("int", 1, 2);
32+
});
2933

30-
test('double', () async {
31-
await testHydrated<double>("double", 1.1, 2.2);
32-
});
34+
test('double', () async {
35+
await testHydrated<double>("double", 1.1, 2.2);
36+
});
3337

34-
test('bool', () async {
35-
await testHydrated<bool>("bool", true, false);
36-
});
38+
test('bool', () async {
39+
await testHydrated<bool>("bool", true, false);
40+
});
3741

38-
test('String', () async {
39-
await testHydrated<String>("String", "first", "second");
40-
});
42+
test('String', () async {
43+
await testHydrated<String>("String", "first", "second");
44+
});
4145

42-
test('List<String>', () async {
43-
testHydrated<List<String>>("List<String>", ["a", "b"], ["c", "d"]);
44-
});
46+
test('List<String>', () async {
47+
testHydrated<List<String>>("List<String>", ["a", "b"], ["c", "d"]);
48+
});
49+
50+
test('SerializedClass', () async {
51+
final completer = Completer();
52+
53+
final subject = HydratedSubject<SerializedClass>(
54+
"SerializedClass",
55+
hydrate: (s) => SerializedClass.fromJSON(s),
56+
persist: (c) => c.toJSON(),
57+
onHydrate: () => completer.complete(),
58+
);
4559

46-
test('SerializedClass', () async {
47-
final completer = Completer();
60+
final second = SerializedClass(false, 42);
4861

62+
/// null before hydrate
63+
expect(subject.valueOrNull, isNull);
64+
65+
/// properly hydrates
66+
await completer.future;
67+
expect(subject.value.value, isTrue);
68+
expect(subject.value.count, equals(42));
69+
70+
/// add values
71+
subject.add(second);
72+
expect(subject.value.value, isFalse);
73+
expect(subject.value.count, equals(42));
74+
75+
/// check value in store
76+
final prefs = await SharedPreferences.getInstance();
77+
expect(prefs.get(subject.key), equals('{"value":false,"count":42}'));
78+
79+
/// clean up
80+
subject.close();
81+
});
82+
});
83+
});
84+
85+
test('HydratedSubject emits latest value into the new listener', () async {
4986
final subject = HydratedSubject<SerializedClass>(
5087
"SerializedClass",
5188
hydrate: (s) => SerializedClass.fromJSON(s),
5289
persist: (c) => c.toJSON(),
53-
onHydrate: () => completer.complete(),
5490
);
5591

56-
final second = SerializedClass(false, 42);
57-
58-
/// null before hydrate
59-
expect(subject.valueOrNull, isNull);
92+
await subject.first;
6093

61-
/// properly hydrates
62-
await completer.future;
63-
expect(subject.value.value, isTrue);
64-
expect(subject.value.count, equals(42));
94+
final expectation = expectLater(
95+
subject.stream,
96+
emitsInOrder([
97+
isA<SerializedClass>().having((c) => c.value, 'value', isTrue),
98+
isA<SerializedClass>().having((c) => c.value, 'value', isFalse),
99+
]));
65100

66-
/// add values
101+
final second = SerializedClass(false, 42);
67102
subject.add(second);
68-
expect(subject.value.value, isFalse);
69-
expect(subject.value.count, equals(42));
70-
71-
/// check value in store
72-
final prefs = await SharedPreferences.getInstance();
73-
expect(prefs.get(subject.key), equals('{"value":false,"count":42}'));
74103

75-
/// clean up
104+
await expectation;
76105
subject.close();
77106
});
78107
}

0 commit comments

Comments
 (0)