From b0021dd9355e6976f092a7ff7a69333cb5f6e8db Mon Sep 17 00:00:00 2001 From: Yurii Prykhodko Date: Wed, 18 Aug 2021 15:35:32 +0300 Subject: [PATCH 01/21] Add support for a generic persistence interface --- example/lib/class_example.dart | 8 +- example/lib/main.dart | 5 +- lib/hydrated.dart | 3 + lib/src/hydrated.dart | 154 ++++++---------- .../generic_value_persistence.dart | 8 + lib/src/persistence/persistence_error.dart | 5 + .../shared_preferences_persistence.dart | 112 ++++++++++++ pubspec.lock | 7 + pubspec.yaml | 1 + test/hydrated_test.dart | 164 ------------------ test/shared_preferences_persistence_test.dart | 131 ++++++++++++++ 11 files changed, 329 insertions(+), 269 deletions(-) create mode 100644 lib/src/persistence/generic_value_persistence.dart create mode 100644 lib/src/persistence/persistence_error.dart create mode 100644 lib/src/persistence/shared_preferences_persistence.dart delete mode 100644 test/hydrated_test.dart create mode 100644 test/shared_preferences_persistence_test.dart diff --git a/example/lib/class_example.dart b/example/lib/class_example.dart index 25bcd34..9056940 100644 --- a/example/lib/class_example.dart +++ b/example/lib/class_example.dart @@ -20,9 +20,11 @@ class MyHomePage extends StatelessWidget { final String _title; final _countSubject = HydratedSubject( - "serialized-count", - hydrate: (value) => SerializedClass.fromJSON(value), - persist: (value) => value.toJSON, + persistence: SharedPreferencesPersistence( + key: "serialized-count", + hydrate: (value) => SerializedClass.fromJSON(value), + persist: (value) => value.toJSON, + ), seedValue: SerializedClass(0), ); diff --git a/example/lib/main.dart b/example/lib/main.dart index 759e904..fafd1d7 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -18,7 +18,10 @@ class MyApp extends StatelessWidget { class MyHomePage extends StatelessWidget { final String _title; - final _count = HydratedSubject("count", seedValue: 0); + final _count = HydratedSubject( + persistence: SharedPreferencesPersistence(key: "count"), + seedValue: 0, + ); MyHomePage({ Key? key, diff --git a/lib/hydrated.dart b/lib/hydrated.dart index 5ff0989..2a200d4 100644 --- a/lib/hydrated.dart +++ b/lib/hydrated.dart @@ -1,3 +1,6 @@ library hydrated; export 'src/hydrated.dart'; +export 'src/persistence/generic_value_persistence.dart'; +export 'src/persistence/persistence_error.dart'; +export 'src/persistence/shared_preferences_persistence.dart'; diff --git a/lib/src/hydrated.dart b/lib/src/hydrated.dart index 77a908b..96f5a89 100644 --- a/lib/src/hydrated.dart +++ b/lib/src/hydrated.dart @@ -1,9 +1,10 @@ import 'dart:async'; import 'package:flutter/foundation.dart'; -import 'package:hydrated/src/utils/type_utils.dart'; +import 'package:hydrated/src/persistence/generic_value_persistence.dart'; +import 'package:hydrated/src/persistence/persistence_error.dart'; +import 'package:hydrated/src/persistence/shared_preferences_persistence.dart'; import 'package:rxdart/rxdart.dart'; -import 'package:shared_preferences/shared_preferences.dart'; /// A callback for encoding an instance of a data class into a String. typedef PersistCallback = String? Function(T); @@ -15,21 +16,8 @@ typedef HydrateCallback = T Function(String); /// /// Mimics the behavior of a [BehaviorSubject]. /// -/// HydratedSubject supports serialized classes and [shared_preferences] types -/// such as: -/// - `int` -/// - `double` -/// - `bool` -/// - `String` -/// - `List`. -/// -/// Serialized classes are supported by using the following `hydrate` and -/// `persist` combination: -/// -/// ``` -/// hydrate: (String)=>Class -/// persist: (Class)=>String -/// ``` +/// The set of supported classes depends on the [GenericValuePersistence] implementation. +/// For a list of types supported by default see [SharedPreferencesPersistence]. /// /// Example: /// @@ -58,48 +46,59 @@ typedef HydrateCallback = T Function(String); /// ); /// ``` class HydratedSubject extends Subject implements ValueStream { - static final _areTypesEqual = TypeUtils.areTypesEqual; final BehaviorSubject _subject; - final String _key; + + @deprecated + // ignore: unused_field final HydrateCallback? _hydrate; + + @deprecated + // ignore: unused_field final PersistCallback? _persist; + final VoidCallback? _onHydrate; final T? _seedValue; + final GenericValuePersistence _persistence; + HydratedSubject._( - this._key, this._seedValue, this._hydrate, this._persist, this._onHydrate, this._subject, + this._persistence, ) : super(_subject, _subject.stream) { _hydrateSubject(); } - factory HydratedSubject( - String key, { + factory HydratedSubject({ + @Deprecated('`key` is deprecated. ' + 'Please use `persistence` for managing storage location.') + String? key, T? seedValue, - HydrateCallback? hydrate, - PersistCallback? persist, + @Deprecated('`hydrate` callback is deprecated. ' + 'Please use `persistence` for storing structures.') + HydrateCallback? hydrate, + @Deprecated('`persist` callback is deprecated. ' + 'Please use `persistence` for storing structures.') + PersistCallback? persist, + + /// An interface for persistent data storage. + /// + /// Defaults to [SharedPreferencesPersistence]. + GenericValuePersistence? persistence, VoidCallback? onHydrate, VoidCallback? onListen, VoidCallback? onCancel, bool sync: false, }) { - // assert that T is a type compatible with shared_preferences, - // or that we have hydrate and persist mapping functions - assert(_areTypesEqual() || - _areTypesEqual() || - _areTypesEqual() || - _areTypesEqual() || - _areTypesEqual() || - _areTypesEqual() || - _areTypesEqual() || - _areTypesEqual() || - _areTypesEqual>() || - _areTypesEqual?>() || - (hydrate != null && persist != null)); + final persistenceInterface = persistence ?? + SharedPreferencesPersistence( + key: key!, + hydrate: hydrate, + persist: persist, + ); // ignore: close_sinks final subject = seedValue != null @@ -116,19 +115,15 @@ class HydratedSubject extends Subject implements ValueStream { ); return HydratedSubject._( - key, seedValue, hydrate, persist, onHydrate, subject, + persistenceInterface, ); } - /// A unique key that references a storage container - /// for a value persisted on the device. - String get key => _key; - @override void onAdd(T event) { _subject.add(event); @@ -167,68 +162,26 @@ class HydratedSubject extends Subject implements ValueStream { /// /// Must be called to retrieve values stored on the device. Future _hydrateSubject() async { - final prefs = await SharedPreferences.getInstance(); - - T? val; + try { + final val = await _persistence.get(); - if (_hydrate != null) { - final String? persistedValue = prefs.getString(_key); - if (persistedValue != null) { - val = _hydrate!(persistedValue); + // do not hydrate if the store is empty or matches the seed value + // TODO: allow writing of seedValue if it is intentional + if (val != null && val != _seedValue) { + _subject.add(val); } - } else if (_areTypesEqual() || _areTypesEqual()) - val = prefs.getInt(_key) as T?; - else if (_areTypesEqual() || _areTypesEqual()) - val = prefs.getDouble(_key) as T?; - else if (_areTypesEqual() || _areTypesEqual()) - val = prefs.getBool(_key) as T?; - else if (_areTypesEqual() || _areTypesEqual()) - val = prefs.getString(_key) as T?; - else if (_areTypesEqual>() || - _areTypesEqual?>()) - val = prefs.getStringList(_key) as T?; - else - Exception( - 'HydratedSubject – shared_preferences returned an invalid type', - ); - - // do not hydrate if the store is empty or matches the seed value - // TODO: allow writing of seedValue if it is intentional - if (val != null && val != _seedValue) { - _subject.add(val); - } - _onHydrate?.call(); + _onHydrate?.call(); + } on PersistenceError catch (e, s) { + addError(e, s); + } } void _persistValue(T val) async { - final prefs = await SharedPreferences.getInstance(); - - if (val is int) - await prefs.setInt(_key, val); - else if (val is double) - await prefs.setDouble(_key, val); - else if (val is bool) - await prefs.setBool(_key, val); - else if (val is String) - await prefs.setString(_key, val); - else if (val is List) - await prefs.setStringList(_key, val); - else if (val == null) - prefs.remove(_key); - else if (_persist != null) { - final encoded = _persist!(val); - if (encoded != null) { - await prefs.setString(_key, encoded); - } else { - prefs.remove(_key); - } - } else { - final error = Exception( - 'HydratedSubject – value must be int, ' - 'double, bool, String, or List', - ); - _subject.addError(error, StackTrace.current); + try { + _persistence.put(val); + } on PersistenceError catch (e, s) { + addError(e, s); } } @@ -237,16 +190,15 @@ class HydratedSubject extends Subject implements ValueStream { VoidCallback? onListen, VoidCallback? onCancel, bool sync = false, + GenericValuePersistence? persistence, HydrateCallback? hydrate, PersistCallback? persist, }) { return HydratedSubject( - _key, onListen: onListen, onCancel: onCancel, sync: sync, - hydrate: hydrate, - persist: persist, + persistence: persistence, ); } } diff --git a/lib/src/persistence/generic_value_persistence.dart b/lib/src/persistence/generic_value_persistence.dart new file mode 100644 index 0000000..e7c49f2 --- /dev/null +++ b/lib/src/persistence/generic_value_persistence.dart @@ -0,0 +1,8 @@ +/// A generic persistence interface for a single [T] value. +abstract class GenericValuePersistence { + /// Save a value to persistence. + Future put(T value); + + /// Retrieve a value from persistence. + Future get(); +} diff --git a/lib/src/persistence/persistence_error.dart b/lib/src/persistence/persistence_error.dart new file mode 100644 index 0000000..b0d9433 --- /dev/null +++ b/lib/src/persistence/persistence_error.dart @@ -0,0 +1,5 @@ +class PersistenceError extends Error { + final String? message; + + PersistenceError(this.message); +} \ No newline at end of file diff --git a/lib/src/persistence/shared_preferences_persistence.dart b/lib/src/persistence/shared_preferences_persistence.dart new file mode 100644 index 0000000..7b9a7f2 --- /dev/null +++ b/lib/src/persistence/shared_preferences_persistence.dart @@ -0,0 +1,112 @@ +import 'package:hydrated/src/persistence/persistence_error.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +import '../utils/type_utils.dart'; +import 'generic_value_persistence.dart'; + +/// A callback for encoding an instance of a data class into a String. +typedef StringEncoder = String? Function(T); + +/// A callback for reconstructing an instance of a data class from a String. +typedef StringDecoder = T Function(String); + +/// An adapter for [SharedPreferences] persistence. +/// +/// Supported types: +/// - `int` +/// - `double` +/// - `bool` +/// - `String` +/// - `List`. +/// - data classes via `hydrate` and `persist` callbacks. +class SharedPreferencesPersistence implements GenericValuePersistence { + final String _key; + final StringDecoder? _hydrate; + final StringEncoder? _persist; + + static final _areTypesEqual = TypeUtils.areTypesEqual; + + SharedPreferencesPersistence({ + required String key, + StringDecoder? hydrate, + StringEncoder? persist, + }) : _key = key, + _hydrate = hydrate, + _persist = persist, + // assert that T is a type compatible with shared_preferences, + // or that we have hydrate and persist mapping functions + assert(_areTypesEqual() || + _areTypesEqual() || + _areTypesEqual() || + _areTypesEqual() || + _areTypesEqual() || + _areTypesEqual() || + _areTypesEqual() || + _areTypesEqual() || + _areTypesEqual>() || + _areTypesEqual?>() || + (hydrate != null && persist != null)); + + @override + Future get() async { + final prefs = await _getPrefs(); + + T? val; + + if (_hydrate != null) { + final String? persistedValue = prefs.getString(_key); + if (persistedValue != null) { + val = _hydrate!(persistedValue); + } + } else if (_areTypesEqual() || _areTypesEqual()) + val = prefs.getInt(_key) as T?; + else if (_areTypesEqual() || _areTypesEqual()) + val = prefs.getDouble(_key) as T?; + else if (_areTypesEqual() || _areTypesEqual()) + val = prefs.getBool(_key) as T?; + else if (_areTypesEqual() || _areTypesEqual()) + val = prefs.getString(_key) as T?; + else if (_areTypesEqual>() || + _areTypesEqual?>()) + val = prefs.getStringList(_key) as T?; + else + throw PersistenceError( + 'Shared Preferences returned an invalid type', + ); + + return val; + } + + @override + Future put(T value) async { + final prefs = await _getPrefs(); + + if (value is int) + await prefs.setInt(_key, value); + else if (value is double) + await prefs.setDouble(_key, value); + else if (value is bool) + await prefs.setBool(_key, value); + else if (value is String) + await prefs.setString(_key, value); + else if (value is List) + await prefs.setStringList(_key, value); + else if (value == null) + prefs.remove(_key); + else if (_persist != null) { + final encoded = _persist!(value); + if (encoded != null) { + await prefs.setString(_key, encoded); + } else { + prefs.remove(_key); + } + } else { + throw PersistenceError( + 'HydratedSubject – value must be int, ' + 'double, bool, String, or List', + ); + } + } + + Future _getPrefs() => SharedPreferences.getInstance(); +} diff --git a/pubspec.lock b/pubspec.lock index 15174fd..6c4c771 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -43,6 +43,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.15.0" + equatable: + dependency: "direct dev" + description: + name: equatable + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.3" fake_async: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 79fb6c0..f01c356 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -16,3 +16,4 @@ dependencies: dev_dependencies: flutter_test: sdk: flutter + equatable: diff --git a/test/hydrated_test.dart b/test/hydrated_test.dart deleted file mode 100644 index 6e2fce2..0000000 --- a/test/hydrated_test.dart +++ /dev/null @@ -1,164 +0,0 @@ -import 'dart:async'; -import 'dart:convert'; - -import 'package:flutter_test/flutter_test.dart'; -import 'package:hydrated/hydrated.dart'; -import 'package:shared_preferences/shared_preferences.dart'; - -void main() { - setUp(() { - SharedPreferences.setMockInitialValues({ - "flutter.prefs": true, - "flutter.int": 1, - "flutter.double": 1.1, - "flutter.bool": true, - "flutter.String": "first", - "flutter.List": ["a", "b"], - "flutter.SerializedClass": '{"value":true,"count":42}' - }); - }); - - test('Shared Preferences set mock initial values', () async { - final prefs = await SharedPreferences.getInstance(); - - final value = prefs.getBool("prefs"); - expect(value, isTrue); - }); - - group('HydratedSubject', () { - group('correctly handles data type', () { - test('int', () async { - await testHydrated("int", 1, 2); - }); - - test('double', () async { - await testHydrated("double", 1.1, 2.2); - }); - - test('bool', () async { - await testHydrated("bool", true, false); - }); - - test('String', () async { - await testHydrated("String", "first", "second"); - }); - - test('List', () async { - testHydrated>("List", ["a", "b"], ["c", "d"]); - }); - - test('SerializedClass', () async { - final completer = Completer(); - - final subject = HydratedSubject( - "SerializedClass", - hydrate: (s) => SerializedClass.fromJSON(s), - persist: (c) => c.toJSON(), - onHydrate: () => completer.complete(), - ); - - final second = SerializedClass(false, 42); - - /// null before hydrate - expect(subject.valueOrNull, isNull); - - /// properly hydrates - await completer.future; - expect(subject.value.value, isTrue); - expect(subject.value.count, equals(42)); - - /// add values - subject.add(second); - expect(subject.value.value, isFalse); - expect(subject.value.count, equals(42)); - - /// check value in store - final prefs = await SharedPreferences.getInstance(); - expect(prefs.get(subject.key), equals('{"value":false,"count":42}')); - - /// clean up - subject.close(); - }); - }); - }); - - test('HydratedSubject emits latest value into the new listener', () async { - final subject = HydratedSubject( - "SerializedClass", - hydrate: (s) => SerializedClass.fromJSON(s), - persist: (c) => c.toJSON(), - ); - - await subject.first; - - final expectation = expectLater( - subject.stream, - emitsInOrder([ - isA().having((c) => c.value, 'value', isTrue), - isA().having((c) => c.value, 'value', isFalse), - ])); - - final second = SerializedClass(false, 42); - subject.add(second); - - await expectation; - subject.close(); - }); -} - -/// An example of a class that serializes to and from a string -class SerializedClass { - final bool value; - final int count; - - SerializedClass(this.value, this.count); - - factory SerializedClass.fromJSON(String s) { - final map = jsonDecode(s); - - return SerializedClass( - map['value'], - map['count'], - ); - } - - String toJSON() => jsonEncode({ - 'value': this.value, - 'count': this.count, - }); -} - -/// The test procedure for a HydratedSubject -Future testHydrated( - String key, - T first, - T second, -) async { - final completer = Completer(); - - final subject = HydratedSubject( - key, - onHydrate: () => completer.complete(), - ); - - /// null before hydrate - expect(subject.valueOrNull, isNull); - expect(subject.hasValue, isFalse); - - /// properly hydrates - await completer.future; - expect(subject.value, equals(first)); - expect(subject.hasValue, isTrue); - - /// add values - subject.add(second); - expect(subject.value, equals(second)); - expect(subject.hasValue, isTrue); - - /// check value in store - final prefs = await SharedPreferences.getInstance(); - expect(prefs.get(subject.key), equals(second)); - - /// clean up - subject.close(); -} diff --git a/test/shared_preferences_persistence_test.dart b/test/shared_preferences_persistence_test.dart new file mode 100644 index 0000000..77b8b11 --- /dev/null +++ b/test/shared_preferences_persistence_test.dart @@ -0,0 +1,131 @@ +import 'dart:async'; +import 'dart:convert'; + +import 'package:equatable/equatable.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:hydrated/hydrated.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +void main() { + setUp(() { + SharedPreferences.setMockInitialValues({}); + }); + + test('Shared Preferences set mock initial values', () async { + final key = 'prefs'; + _setMockPersistedValue(key, true); + + final prefs = await SharedPreferences.getInstance(); + final value = prefs.getBool(key); + expect(value, isTrue); + }); + + group('HydratedSubject', () { + group('correctly handles data type', () { + test('int', () async { + await _testPersistence("int", 1, 2); + }); + + test('double', () async { + await _testPersistence("double", 1.1, 2.2); + }); + + test('bool', () async { + await _testPersistence("bool", true, false); + }); + + test('String', () async { + await _testPersistence("String", "first", "second"); + }); + + test('List', () async { + _testPersistence?>("List", ["a", "b"], ["c", "d"]); + }); + + test('SerializedClass', () async { + const key = 'SerializedClass'; + final first = SerializedClass(true, 24); + _setMockPersistedValue(key, first.toJSON()); + + final persistence = SharedPreferencesPersistence( + key: key, + hydrate: (s) => SerializedClass.fromJSON(s), + persist: (c) => c.toJSON(), + ); + + final second = SerializedClass(false, 42); + + /// restores from pre-existing persisted value + expect(await persistence.get(), equals(first)); + + /// persist a new value + persistence.put(second); + expect(await persistence.get(), equals(second)); + + /// check shared_preferences stored value + final prefs = await SharedPreferences.getInstance(); + expect(prefs.get(key), equals('{"value":false,"count":42}')); + }); + }); + }); +} + +/// An example of a class that serializes to and from a string +class SerializedClass extends Equatable { + final bool value; + final int count; + + SerializedClass(this.value, this.count); + + factory SerializedClass.fromJSON(String s) { + final map = jsonDecode(s); + + return SerializedClass( + map['value'], + map['count'], + ); + } + + String toJSON() => jsonEncode({ + 'value': this.value, + 'count': this.count, + }); + + @override + List get props => [value, count]; +} + +void _setMockPersistedValue(String key, dynamic value) { + SharedPreferences.setMockInitialValues({ + "flutter.$key": value, + }); +} + +/// The test procedure for a HydratedSubject +Future _testPersistence( + String key, + T first, + T second, +) async { + final persistence = SharedPreferencesPersistence(key: key); + + /// null before setting anything + expect(await persistence.get(), isNull); + + _setMockPersistedValue(key, first); + + /// restores from pre-existing persisted value + expect(await persistence.get(), equals(first)); + + /// persists a new value + await persistence.put(second); + expect(await persistence.get(), equals(second)); + + /// check shared_preferences stored value + final prefs = await SharedPreferences.getInstance(); + expect(prefs.get(key), equals(second)); + + /// remove persisted value + await persistence.put(null as T); + expect(await persistence.get(), isNull); +} From 60a5bc2ea970e94e2b2f8bea49ba56aed2a726bf Mon Sep 17 00:00:00 2001 From: Yurii Prykhodko Date: Thu, 19 Aug 2021 18:51:31 +0300 Subject: [PATCH 02/21] Add a different constructor for using a custom storage, revert breaking changes --- example/lib/class_example.dart | 8 ++-- example/lib/main.dart | 5 +-- lib/src/hydrated.dart | 72 +++++++++++++++------------------- 3 files changed, 35 insertions(+), 50 deletions(-) diff --git a/example/lib/class_example.dart b/example/lib/class_example.dart index 9056940..25bcd34 100644 --- a/example/lib/class_example.dart +++ b/example/lib/class_example.dart @@ -20,11 +20,9 @@ class MyHomePage extends StatelessWidget { final String _title; final _countSubject = HydratedSubject( - persistence: SharedPreferencesPersistence( - key: "serialized-count", - hydrate: (value) => SerializedClass.fromJSON(value), - persist: (value) => value.toJSON, - ), + "serialized-count", + hydrate: (value) => SerializedClass.fromJSON(value), + persist: (value) => value.toJSON, seedValue: SerializedClass(0), ); diff --git a/example/lib/main.dart b/example/lib/main.dart index fafd1d7..759e904 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -18,10 +18,7 @@ class MyApp extends StatelessWidget { class MyHomePage extends StatelessWidget { final String _title; - final _count = HydratedSubject( - persistence: SharedPreferencesPersistence(key: "count"), - seedValue: 0, - ); + final _count = HydratedSubject("count", seedValue: 0); MyHomePage({ Key? key, diff --git a/lib/src/hydrated.dart b/lib/src/hydrated.dart index 96f5a89..a96288d 100644 --- a/lib/src/hydrated.dart +++ b/lib/src/hydrated.dart @@ -47,15 +47,6 @@ typedef HydrateCallback = T Function(String); /// ``` class HydratedSubject extends Subject implements ValueStream { final BehaviorSubject _subject; - - @deprecated - // ignore: unused_field - final HydrateCallback? _hydrate; - - @deprecated - // ignore: unused_field - final PersistCallback? _persist; - final VoidCallback? _onHydrate; final T? _seedValue; @@ -63,8 +54,6 @@ class HydratedSubject extends Subject implements ValueStream { HydratedSubject._( this._seedValue, - this._hydrate, - this._persist, this._onHydrate, this._subject, this._persistence, @@ -72,34 +61,14 @@ class HydratedSubject extends Subject implements ValueStream { _hydrateSubject(); } - factory HydratedSubject({ - @Deprecated('`key` is deprecated. ' - 'Please use `persistence` for managing storage location.') - String? key, + factory HydratedSubject.custom({ + required GenericValuePersistence persistence, T? seedValue, - @Deprecated('`hydrate` callback is deprecated. ' - 'Please use `persistence` for storing structures.') - HydrateCallback? hydrate, - @Deprecated('`persist` callback is deprecated. ' - 'Please use `persistence` for storing structures.') - PersistCallback? persist, - - /// An interface for persistent data storage. - /// - /// Defaults to [SharedPreferencesPersistence]. - GenericValuePersistence? persistence, VoidCallback? onHydrate, VoidCallback? onListen, VoidCallback? onCancel, - bool sync: false, + bool sync = false, }) { - final persistenceInterface = persistence ?? - SharedPreferencesPersistence( - key: key!, - hydrate: hydrate, - persist: persist, - ); - // ignore: close_sinks final subject = seedValue != null ? BehaviorSubject.seeded( @@ -116,11 +85,34 @@ class HydratedSubject extends Subject implements ValueStream { return HydratedSubject._( seedValue, - hydrate, - persist, onHydrate, subject, - persistenceInterface, + persistence, + ); + } + + factory HydratedSubject( + String key, { + T? seedValue, + HydrateCallback? hydrate, + PersistCallback? persist, + VoidCallback? onHydrate, + VoidCallback? onListen, + VoidCallback? onCancel, + bool sync: false, + }) { + final persistenceInterface = SharedPreferencesPersistence( + key: key, + hydrate: hydrate, + persist: persist, + ); + + return HydratedSubject.custom( + persistence: persistenceInterface, + onHydrate: onHydrate, + onCancel: onCancel, + onListen: onListen, + sync: sync, ); } @@ -191,14 +183,12 @@ class HydratedSubject extends Subject implements ValueStream { VoidCallback? onCancel, bool sync = false, GenericValuePersistence? persistence, - HydrateCallback? hydrate, - PersistCallback? persist, }) { - return HydratedSubject( + return HydratedSubject.custom( onListen: onListen, onCancel: onCancel, sync: sync, - persistence: persistence, + persistence: persistence!, ); } } From ba44dcb33391faf2dd4749598fd1ac84f5cf0585 Mon Sep 17 00:00:00 2001 From: Yurii Prykhodko Date: Thu, 19 Aug 2021 19:24:47 +0300 Subject: [PATCH 03/21] Keep key, hydrate and persist callbacks in the HydratedSubject --- lib/src/hydrated.dart | 73 ++++++++------- .../generic_value_persistence.dart | 6 +- .../shared_preferences_persistence.dart | 93 +++++++------------ test/shared_preferences_persistence_test.dart | 39 ++------ 4 files changed, 84 insertions(+), 127 deletions(-) diff --git a/lib/src/hydrated.dart b/lib/src/hydrated.dart index a96288d..4f46d89 100644 --- a/lib/src/hydrated.dart +++ b/lib/src/hydrated.dart @@ -46,14 +46,20 @@ typedef HydrateCallback = T Function(String); /// ); /// ``` class HydratedSubject extends Subject implements ValueStream { + final String _key; + final HydrateCallback? _hydrate; + final PersistCallback? _persist; final BehaviorSubject _subject; final VoidCallback? _onHydrate; final T? _seedValue; - final GenericValuePersistence _persistence; + final GenericValuePersistence _persistence; HydratedSubject._( + this._key, this._seedValue, + this._hydrate, + this._persist, this._onHydrate, this._subject, this._persistence, @@ -61,13 +67,16 @@ class HydratedSubject extends Subject implements ValueStream { _hydrateSubject(); } - factory HydratedSubject.custom({ - required GenericValuePersistence persistence, + factory HydratedSubject( + String key, { T? seedValue, + HydrateCallback? hydrate, + PersistCallback? persist, VoidCallback? onHydrate, VoidCallback? onListen, VoidCallback? onCancel, bool sync = false, + GenericValuePersistence persistence = const SharedPreferencesPersistence(), }) { // ignore: close_sinks final subject = seedValue != null @@ -84,38 +93,16 @@ class HydratedSubject extends Subject implements ValueStream { ); return HydratedSubject._( + key, seedValue, + hydrate, + persist, onHydrate, subject, persistence, ); } - factory HydratedSubject( - String key, { - T? seedValue, - HydrateCallback? hydrate, - PersistCallback? persist, - VoidCallback? onHydrate, - VoidCallback? onListen, - VoidCallback? onCancel, - bool sync: false, - }) { - final persistenceInterface = SharedPreferencesPersistence( - key: key, - hydrate: hydrate, - persist: persist, - ); - - return HydratedSubject.custom( - persistence: persistenceInterface, - onHydrate: onHydrate, - onCancel: onCancel, - onListen: onListen, - sync: sync, - ); - } - @override void onAdd(T event) { _subject.add(event); @@ -150,12 +137,22 @@ class HydratedSubject extends Subject implements ValueStream { @override StackTrace? get stackTrace => _subject.stackTrace; + bool get _doEncodePersistedValue => _hydrate != null && _persist != null; + /// Hydrates the HydratedSubject with a value stored on the user's device. /// /// Must be called to retrieve values stored on the device. Future _hydrateSubject() async { try { - final val = await _persistence.get(); + T? val; + if (_doEncodePersistedValue) { + final encodedValue = await _persistence.get(_key); + if (encodedValue != null) { + val = _hydrate!(encodedValue); + } + } else { + val = await _persistence.get(_key); + } // do not hydrate if the store is empty or matches the seed value // TODO: allow writing of seedValue if it is intentional @@ -171,7 +168,14 @@ class HydratedSubject extends Subject implements ValueStream { void _persistValue(T val) async { try { - _persistence.put(val); + var persistedVal; + if (_doEncodePersistedValue) { + persistedVal = _persist!(val); + _persistence.put(_key, persistedVal); + } else { + persistedVal = val; + _persistence.put(_key, persistedVal); + } } on PersistenceError catch (e, s) { addError(e, s); } @@ -182,13 +186,16 @@ class HydratedSubject extends Subject implements ValueStream { VoidCallback? onListen, VoidCallback? onCancel, bool sync = false, - GenericValuePersistence? persistence, + HydrateCallback? hydrate, + PersistCallback? persist, }) { - return HydratedSubject.custom( + return HydratedSubject( + _key, onListen: onListen, onCancel: onCancel, sync: sync, - persistence: persistence!, + hydrate: hydrate, + persist: persist, ); } } diff --git a/lib/src/persistence/generic_value_persistence.dart b/lib/src/persistence/generic_value_persistence.dart index e7c49f2..21f81bc 100644 --- a/lib/src/persistence/generic_value_persistence.dart +++ b/lib/src/persistence/generic_value_persistence.dart @@ -1,8 +1,8 @@ /// A generic persistence interface for a single [T] value. -abstract class GenericValuePersistence { +abstract class GenericValuePersistence { /// Save a value to persistence. - Future put(T value); + Future put(String key, T value); /// Retrieve a value from persistence. - Future get(); + Future get(String key); } diff --git a/lib/src/persistence/shared_preferences_persistence.dart b/lib/src/persistence/shared_preferences_persistence.dart index 7b9a7f2..bd795b2 100644 --- a/lib/src/persistence/shared_preferences_persistence.dart +++ b/lib/src/persistence/shared_preferences_persistence.dart @@ -4,12 +4,6 @@ import 'package:shared_preferences/shared_preferences.dart'; import '../utils/type_utils.dart'; import 'generic_value_persistence.dart'; -/// A callback for encoding an instance of a data class into a String. -typedef StringEncoder = String? Function(T); - -/// A callback for reconstructing an instance of a data class from a String. -typedef StringDecoder = T Function(String); - /// An adapter for [SharedPreferences] persistence. /// /// Supported types: @@ -18,57 +12,29 @@ typedef StringDecoder = T Function(String); /// - `bool` /// - `String` /// - `List`. -/// - data classes via `hydrate` and `persist` callbacks. -class SharedPreferencesPersistence implements GenericValuePersistence { - final String _key; - final StringDecoder? _hydrate; - final StringEncoder? _persist; - +class SharedPreferencesPersistence implements GenericValuePersistence { static final _areTypesEqual = TypeUtils.areTypesEqual; - SharedPreferencesPersistence({ - required String key, - StringDecoder? hydrate, - StringEncoder? persist, - }) : _key = key, - _hydrate = hydrate, - _persist = persist, - // assert that T is a type compatible with shared_preferences, - // or that we have hydrate and persist mapping functions - assert(_areTypesEqual() || - _areTypesEqual() || - _areTypesEqual() || - _areTypesEqual() || - _areTypesEqual() || - _areTypesEqual() || - _areTypesEqual() || - _areTypesEqual() || - _areTypesEqual>() || - _areTypesEqual?>() || - (hydrate != null && persist != null)); + const SharedPreferencesPersistence(); @override - Future get() async { + Future get(String key) async { + _assertSupportedType(); final prefs = await _getPrefs(); T? val; - if (_hydrate != null) { - final String? persistedValue = prefs.getString(_key); - if (persistedValue != null) { - val = _hydrate!(persistedValue); - } - } else if (_areTypesEqual() || _areTypesEqual()) - val = prefs.getInt(_key) as T?; + if (_areTypesEqual() || _areTypesEqual()) + val = prefs.getInt(key) as T?; else if (_areTypesEqual() || _areTypesEqual()) - val = prefs.getDouble(_key) as T?; + val = prefs.getDouble(key) as T?; else if (_areTypesEqual() || _areTypesEqual()) - val = prefs.getBool(_key) as T?; + val = prefs.getBool(key) as T?; else if (_areTypesEqual() || _areTypesEqual()) - val = prefs.getString(_key) as T?; + val = prefs.getString(key) as T?; else if (_areTypesEqual>() || _areTypesEqual?>()) - val = prefs.getStringList(_key) as T?; + val = prefs.getStringList(key) as T?; else throw PersistenceError( 'Shared Preferences returned an invalid type', @@ -78,29 +44,23 @@ class SharedPreferencesPersistence implements GenericValuePersistence { } @override - Future put(T value) async { + Future put(String key, T value) async { + _assertSupportedType(); final prefs = await _getPrefs(); if (value is int) - await prefs.setInt(_key, value); + await prefs.setInt(key, value); else if (value is double) - await prefs.setDouble(_key, value); + await prefs.setDouble(key, value); else if (value is bool) - await prefs.setBool(_key, value); + await prefs.setBool(key, value); else if (value is String) - await prefs.setString(_key, value); + await prefs.setString(key, value); else if (value is List) - await prefs.setStringList(_key, value); + await prefs.setStringList(key, value); else if (value == null) - prefs.remove(_key); - else if (_persist != null) { - final encoded = _persist!(value); - if (encoded != null) { - await prefs.setString(_key, encoded); - } else { - prefs.remove(_key); - } - } else { + prefs.remove(key); + else { throw PersistenceError( 'HydratedSubject – value must be int, ' 'double, bool, String, or List', @@ -108,5 +68,20 @@ class SharedPreferencesPersistence implements GenericValuePersistence { } } + void _assertSupportedType() { + assert( + _areTypesEqual() || + _areTypesEqual() || + _areTypesEqual() || + _areTypesEqual() || + _areTypesEqual() || + _areTypesEqual() || + _areTypesEqual() || + _areTypesEqual() || + _areTypesEqual>() || + _areTypesEqual?>(), + '$T type is not supported by SharedPreferences.'); + } + Future _getPrefs() => SharedPreferences.getInstance(); } diff --git a/test/shared_preferences_persistence_test.dart b/test/shared_preferences_persistence_test.dart index 77b8b11..690b427 100644 --- a/test/shared_preferences_persistence_test.dart +++ b/test/shared_preferences_persistence_test.dart @@ -41,31 +41,6 @@ void main() { test('List', () async { _testPersistence?>("List", ["a", "b"], ["c", "d"]); }); - - test('SerializedClass', () async { - const key = 'SerializedClass'; - final first = SerializedClass(true, 24); - _setMockPersistedValue(key, first.toJSON()); - - final persistence = SharedPreferencesPersistence( - key: key, - hydrate: (s) => SerializedClass.fromJSON(s), - persist: (c) => c.toJSON(), - ); - - final second = SerializedClass(false, 42); - - /// restores from pre-existing persisted value - expect(await persistence.get(), equals(first)); - - /// persist a new value - persistence.put(second); - expect(await persistence.get(), equals(second)); - - /// check shared_preferences stored value - final prefs = await SharedPreferences.getInstance(); - expect(prefs.get(key), equals('{"value":false,"count":42}')); - }); }); }); } @@ -107,25 +82,25 @@ Future _testPersistence( T first, T second, ) async { - final persistence = SharedPreferencesPersistence(key: key); + final persistence = SharedPreferencesPersistence(); /// null before setting anything - expect(await persistence.get(), isNull); + expect(await persistence.get(key), isNull); _setMockPersistedValue(key, first); /// restores from pre-existing persisted value - expect(await persistence.get(), equals(first)); + expect(await persistence.get(key), equals(first)); /// persists a new value - await persistence.put(second); - expect(await persistence.get(), equals(second)); + await persistence.put(key, second); + expect(await persistence.get(key), equals(second)); /// check shared_preferences stored value final prefs = await SharedPreferences.getInstance(); expect(prefs.get(key), equals(second)); /// remove persisted value - await persistence.put(null as T); - expect(await persistence.get(), isNull); + await persistence.put(key, null as T); + expect(await persistence.get(key), isNull); } From dfb8401d1415059b16ad6dedabe2cad8ddaf9886 Mon Sep 17 00:00:00 2001 From: Yurii Prykhodko Date: Thu, 19 Aug 2021 19:48:47 +0300 Subject: [PATCH 04/21] Rename GenericValuePersistence -> KeyValueStore --- lib/hydrated.dart | 2 +- lib/src/hydrated.dart | 8 ++++---- ...eneric_value_persistence.dart => key_value_store.dart} | 4 ++-- lib/src/persistence/shared_preferences_persistence.dart | 4 ++-- 4 files changed, 9 insertions(+), 9 deletions(-) rename lib/src/persistence/{generic_value_persistence.dart => key_value_store.dart} (60%) diff --git a/lib/hydrated.dart b/lib/hydrated.dart index 2a200d4..7840d86 100644 --- a/lib/hydrated.dart +++ b/lib/hydrated.dart @@ -1,6 +1,6 @@ library hydrated; export 'src/hydrated.dart'; -export 'src/persistence/generic_value_persistence.dart'; +export 'src/persistence/key_value_store.dart'; export 'src/persistence/persistence_error.dart'; export 'src/persistence/shared_preferences_persistence.dart'; diff --git a/lib/src/hydrated.dart b/lib/src/hydrated.dart index 4f46d89..64d3074 100644 --- a/lib/src/hydrated.dart +++ b/lib/src/hydrated.dart @@ -1,7 +1,7 @@ import 'dart:async'; import 'package:flutter/foundation.dart'; -import 'package:hydrated/src/persistence/generic_value_persistence.dart'; +import 'package:hydrated/src/persistence/key_value_store.dart'; import 'package:hydrated/src/persistence/persistence_error.dart'; import 'package:hydrated/src/persistence/shared_preferences_persistence.dart'; import 'package:rxdart/rxdart.dart'; @@ -16,7 +16,7 @@ typedef HydrateCallback = T Function(String); /// /// Mimics the behavior of a [BehaviorSubject]. /// -/// The set of supported classes depends on the [GenericValuePersistence] implementation. +/// The set of supported classes depends on the [KeyValueStore] implementation. /// For a list of types supported by default see [SharedPreferencesPersistence]. /// /// Example: @@ -53,7 +53,7 @@ class HydratedSubject extends Subject implements ValueStream { final VoidCallback? _onHydrate; final T? _seedValue; - final GenericValuePersistence _persistence; + final KeyValueStore _persistence; HydratedSubject._( this._key, @@ -76,7 +76,7 @@ class HydratedSubject extends Subject implements ValueStream { VoidCallback? onListen, VoidCallback? onCancel, bool sync = false, - GenericValuePersistence persistence = const SharedPreferencesPersistence(), + KeyValueStore persistence = const SharedPreferencesPersistence(), }) { // ignore: close_sinks final subject = seedValue != null diff --git a/lib/src/persistence/generic_value_persistence.dart b/lib/src/persistence/key_value_store.dart similarity index 60% rename from lib/src/persistence/generic_value_persistence.dart rename to lib/src/persistence/key_value_store.dart index 21f81bc..47c5ee6 100644 --- a/lib/src/persistence/generic_value_persistence.dart +++ b/lib/src/persistence/key_value_store.dart @@ -1,5 +1,5 @@ -/// A generic persistence interface for a single [T] value. -abstract class GenericValuePersistence { +/// A generic key-value persistence interface. +abstract class KeyValueStore { /// Save a value to persistence. Future put(String key, T value); diff --git a/lib/src/persistence/shared_preferences_persistence.dart b/lib/src/persistence/shared_preferences_persistence.dart index bd795b2..0fd3326 100644 --- a/lib/src/persistence/shared_preferences_persistence.dart +++ b/lib/src/persistence/shared_preferences_persistence.dart @@ -2,7 +2,7 @@ import 'package:hydrated/src/persistence/persistence_error.dart'; import 'package:shared_preferences/shared_preferences.dart'; import '../utils/type_utils.dart'; -import 'generic_value_persistence.dart'; +import 'key_value_store.dart'; /// An adapter for [SharedPreferences] persistence. /// @@ -12,7 +12,7 @@ import 'generic_value_persistence.dart'; /// - `bool` /// - `String` /// - `List`. -class SharedPreferencesPersistence implements GenericValuePersistence { +class SharedPreferencesPersistence implements KeyValueStore { static final _areTypesEqual = TypeUtils.areTypesEqual; const SharedPreferencesPersistence(); From de48f0b0fa723ed1feeedaac862cd24f9307ee62 Mon Sep 17 00:00:00 2001 From: Yurii Prykhodko Date: Thu, 19 Aug 2021 19:51:00 +0300 Subject: [PATCH 05/21] Make KeyValueStore.put always accept nullables. --- lib/src/persistence/key_value_store.dart | 4 +++- lib/src/persistence/shared_preferences_persistence.dart | 8 ++++---- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/lib/src/persistence/key_value_store.dart b/lib/src/persistence/key_value_store.dart index 47c5ee6..9fddfeb 100644 --- a/lib/src/persistence/key_value_store.dart +++ b/lib/src/persistence/key_value_store.dart @@ -1,7 +1,9 @@ /// A generic key-value persistence interface. abstract class KeyValueStore { /// Save a value to persistence. - Future put(String key, T value); + /// + /// Passing a `null` should clear the value for this [key]. + Future put(String key, T? value); /// Retrieve a value from persistence. Future get(String key); diff --git a/lib/src/persistence/shared_preferences_persistence.dart b/lib/src/persistence/shared_preferences_persistence.dart index 0fd3326..01ad090 100644 --- a/lib/src/persistence/shared_preferences_persistence.dart +++ b/lib/src/persistence/shared_preferences_persistence.dart @@ -44,11 +44,13 @@ class SharedPreferencesPersistence implements KeyValueStore { } @override - Future put(String key, T value) async { + Future put(String key, T? value) async { _assertSupportedType(); final prefs = await _getPrefs(); - if (value is int) + if (value == null) + prefs.remove(key); + else if (value is int) await prefs.setInt(key, value); else if (value is double) await prefs.setDouble(key, value); @@ -58,8 +60,6 @@ class SharedPreferencesPersistence implements KeyValueStore { await prefs.setString(key, value); else if (value is List) await prefs.setStringList(key, value); - else if (value == null) - prefs.remove(key); else { throw PersistenceError( 'HydratedSubject – value must be int, ' From da6ad169b4f663603cbb3450933c852fbbed0b5d Mon Sep 17 00:00:00 2001 From: Yurii Prykhodko Date: Thu, 19 Aug 2021 19:55:12 +0300 Subject: [PATCH 06/21] Consistently use local imports --- lib/src/hydrated.dart | 7 ++++--- lib/src/persistence/shared_preferences_persistence.dart | 2 +- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/lib/src/hydrated.dart b/lib/src/hydrated.dart index 64d3074..9c1a15c 100644 --- a/lib/src/hydrated.dart +++ b/lib/src/hydrated.dart @@ -1,11 +1,12 @@ import 'dart:async'; import 'package:flutter/foundation.dart'; -import 'package:hydrated/src/persistence/key_value_store.dart'; -import 'package:hydrated/src/persistence/persistence_error.dart'; -import 'package:hydrated/src/persistence/shared_preferences_persistence.dart'; import 'package:rxdart/rxdart.dart'; +import 'persistence/key_value_store.dart'; +import 'persistence/persistence_error.dart'; +import 'persistence/shared_preferences_persistence.dart'; + /// A callback for encoding an instance of a data class into a String. typedef PersistCallback = String? Function(T); diff --git a/lib/src/persistence/shared_preferences_persistence.dart b/lib/src/persistence/shared_preferences_persistence.dart index 01ad090..71183c8 100644 --- a/lib/src/persistence/shared_preferences_persistence.dart +++ b/lib/src/persistence/shared_preferences_persistence.dart @@ -1,8 +1,8 @@ -import 'package:hydrated/src/persistence/persistence_error.dart'; import 'package:shared_preferences/shared_preferences.dart'; import '../utils/type_utils.dart'; import 'key_value_store.dart'; +import 'persistence_error.dart'; /// An adapter for [SharedPreferences] persistence. /// From 55ac46b03a77a4137a9a0215be00c9a69257ea28 Mon Sep 17 00:00:00 2001 From: Yurii Prykhodko Date: Thu, 19 Aug 2021 19:55:39 +0300 Subject: [PATCH 07/21] Add missing newline --- lib/src/persistence/persistence_error.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/src/persistence/persistence_error.dart b/lib/src/persistence/persistence_error.dart index b0d9433..cbd7213 100644 --- a/lib/src/persistence/persistence_error.dart +++ b/lib/src/persistence/persistence_error.dart @@ -2,4 +2,4 @@ class PersistenceError extends Error { final String? message; PersistenceError(this.message); -} \ No newline at end of file +} From e3728f38c7d3732e8070b13f57bf97830b5cf271 Mon Sep 17 00:00:00 2001 From: Yurii Prykhodko Date: Mon, 23 Aug 2021 18:50:28 +0300 Subject: [PATCH 08/21] Restore `key` getter --- lib/src/hydrated.dart | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/src/hydrated.dart b/lib/src/hydrated.dart index 9c1a15c..667fe07 100644 --- a/lib/src/hydrated.dart +++ b/lib/src/hydrated.dart @@ -56,6 +56,10 @@ class HydratedSubject extends Subject implements ValueStream { final KeyValueStore _persistence; + /// A unique key that references a storage container + /// for a value persisted on the device. + String get key => _key; + HydratedSubject._( this._key, this._seedValue, From 41f9310a8d081612409538400084c4fd89d036ab Mon Sep 17 00:00:00 2001 From: Yurii Prykhodko Date: Mon, 23 Aug 2021 18:56:21 +0300 Subject: [PATCH 09/21] Readability improvements --- lib/src/hydrated.dart | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/lib/src/hydrated.dart b/lib/src/hydrated.dart index 667fe07..65eee4f 100644 --- a/lib/src/hydrated.dart +++ b/lib/src/hydrated.dart @@ -142,18 +142,16 @@ class HydratedSubject extends Subject implements ValueStream { @override StackTrace? get stackTrace => _subject.stackTrace; - bool get _doEncodePersistedValue => _hydrate != null && _persist != null; - /// Hydrates the HydratedSubject with a value stored on the user's device. /// /// Must be called to retrieve values stored on the device. Future _hydrateSubject() async { try { T? val; - if (_doEncodePersistedValue) { - final encodedValue = await _persistence.get(_key); - if (encodedValue != null) { - val = _hydrate!(encodedValue); + if (_hydrate != null) { + final persistedValue = await _persistence.get(_key); + if (persistedValue != null) { + val = _hydrate!(persistedValue); } } else { val = await _persistence.get(_key); @@ -174,7 +172,7 @@ class HydratedSubject extends Subject implements ValueStream { void _persistValue(T val) async { try { var persistedVal; - if (_doEncodePersistedValue) { + if (_persist != null) { persistedVal = _persist!(val); _persistence.put(_key, persistedVal); } else { From 986b272c8350a7cd0dc8e0bac16c4f918b3db3d3 Mon Sep 17 00:00:00 2001 From: Yurii Prykhodko Date: Mon, 23 Aug 2021 19:08:21 +0300 Subject: [PATCH 10/21] Add code docs --- lib/src/persistence/key_value_store.dart | 6 ++++++ lib/src/persistence/persistence_error.dart | 5 ++++- lib/src/utils/type_utils.dart | 10 ++++++++++ 3 files changed, 20 insertions(+), 1 deletion(-) diff --git a/lib/src/persistence/key_value_store.dart b/lib/src/persistence/key_value_store.dart index 9fddfeb..0f38ca2 100644 --- a/lib/src/persistence/key_value_store.dart +++ b/lib/src/persistence/key_value_store.dart @@ -1,10 +1,16 @@ +import 'persistence_error.dart'; + /// A generic key-value persistence interface. abstract class KeyValueStore { /// Save a value to persistence. /// /// Passing a `null` should clear the value for this [key]. + /// + /// Throw a [PersistenceError] if encountering a problem while persisting a value. Future put(String key, T? value); /// Retrieve a value from persistence. + /// + /// Throw a [PersistenceError] if encountering a problem while restoring a value from the storage. Future get(String key); } diff --git a/lib/src/persistence/persistence_error.dart b/lib/src/persistence/persistence_error.dart index cbd7213..2343a9b 100644 --- a/lib/src/persistence/persistence_error.dart +++ b/lib/src/persistence/persistence_error.dart @@ -1,5 +1,8 @@ +/// An error encountered when persisting a value, or restoring it from persistence. class PersistenceError extends Error { - final String? message; + /// A description of an error. + final String message; + /// A persistence error with a [message] describing its details. PersistenceError(this.message); } diff --git a/lib/src/utils/type_utils.dart b/lib/src/utils/type_utils.dart index df0c94c..46d0cd5 100644 --- a/lib/src/utils/type_utils.dart +++ b/lib/src/utils/type_utils.dart @@ -1,4 +1,14 @@ +/// Utilities for working with Dart type system. class TypeUtils { + /// Check two types for equality. + /// + /// Returns `true` if types match exactly, taking nullable types into account. + /// + /// Example outputs: + /// ``` + /// TypeUtils.areTypesEqual() == true + /// TypeUtils.areTypesEqual() == false + /// ``` static bool areTypesEqual() { return T1 == T2; } From 8e6843e4ff81b580ad9961486654e91ea4844e30 Mon Sep 17 00:00:00 2001 From: Yurii Prykhodko Date: Mon, 23 Aug 2021 20:33:55 +0300 Subject: [PATCH 11/21] Bring in BehaviorSubject test suite from rxdart repo --- lib/src/hydrated.dart | 5 +- pubspec.lock | 32 +- pubspec.yaml | 1 + test/hydrated_behavior_test.dart | 1103 ++++++++++++++++++++++++++++++ 4 files changed, 1122 insertions(+), 19 deletions(-) create mode 100644 test/hydrated_behavior_test.dart diff --git a/lib/src/hydrated.dart b/lib/src/hydrated.dart index 65eee4f..d3c5ce8 100644 --- a/lib/src/hydrated.dart +++ b/lib/src/hydrated.dart @@ -110,7 +110,6 @@ class HydratedSubject extends Subject implements ValueStream { @override void onAdd(T event) { - _subject.add(event); _persistValue(event); } @@ -128,7 +127,7 @@ class HydratedSubject extends Subject implements ValueStream { T get value => _subject.value; /// Set and emit the new value - set value(T newValue) => add(value); + set value(T newValue) => add(newValue); @override Object get error => _subject.error; @@ -154,7 +153,7 @@ class HydratedSubject extends Subject implements ValueStream { val = _hydrate!(persistedValue); } } else { - val = await _persistence.get(_key); + val = await _persistence.get(_key); } // do not hydrate if the store is empty or matches the seed value diff --git a/pubspec.lock b/pubspec.lock index 6c4c771..473dd96 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -63,14 +63,14 @@ packages: name: ffi url: "https://pub.dartlang.org" source: hosted - version: "1.0.0" + version: "1.1.2" file: dependency: transitive description: name: file url: "https://pub.dartlang.org" source: hosted - version: "6.1.0" + version: "6.1.2" flutter: dependency: "direct main" description: flutter @@ -120,42 +120,42 @@ packages: name: path_provider_linux url: "https://pub.dartlang.org" source: hosted - version: "2.0.0" + version: "2.0.2" path_provider_platform_interface: dependency: transitive description: name: path_provider_platform_interface url: "https://pub.dartlang.org" source: hosted - version: "2.0.0" + version: "2.0.1" path_provider_windows: dependency: transitive description: name: path_provider_windows url: "https://pub.dartlang.org" source: hosted - version: "2.0.0" + version: "2.0.3" platform: dependency: transitive description: name: platform url: "https://pub.dartlang.org" source: hosted - version: "3.0.0" + version: "3.0.2" plugin_platform_interface: dependency: transitive description: name: plugin_platform_interface url: "https://pub.dartlang.org" source: hosted - version: "2.0.0" + version: "2.0.1" process: dependency: transitive description: name: process url: "https://pub.dartlang.org" source: hosted - version: "4.1.0" + version: "4.2.3" rxdart: dependency: "direct main" description: @@ -169,21 +169,21 @@ packages: name: shared_preferences url: "https://pub.dartlang.org" source: hosted - version: "2.0.1" + version: "2.0.7" shared_preferences_linux: dependency: transitive description: name: shared_preferences_linux url: "https://pub.dartlang.org" source: hosted - version: "2.0.0" + version: "2.0.2" shared_preferences_macos: dependency: transitive description: name: shared_preferences_macos url: "https://pub.dartlang.org" source: hosted - version: "2.0.0" + version: "2.0.2" shared_preferences_platform_interface: dependency: transitive description: @@ -197,14 +197,14 @@ packages: name: shared_preferences_web url: "https://pub.dartlang.org" source: hosted - version: "2.0.0" + version: "2.0.2" shared_preferences_windows: dependency: transitive description: name: shared_preferences_windows url: "https://pub.dartlang.org" source: hosted - version: "2.0.0" + version: "2.0.2" sky_engine: dependency: transitive description: flutter @@ -272,7 +272,7 @@ packages: name: win32 url: "https://pub.dartlang.org" source: hosted - version: "2.0.0" + version: "2.2.5" xdg_directories: dependency: transitive description: @@ -281,5 +281,5 @@ packages: source: hosted version: "0.2.0" sdks: - dart: ">=2.12.0 <3.0.0" - flutter: ">=1.20.0" + dart: ">=2.13.0 <3.0.0" + flutter: ">=2.0.0" diff --git a/pubspec.yaml b/pubspec.yaml index f01c356..77d64ba 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -17,3 +17,4 @@ dev_dependencies: flutter_test: sdk: flutter equatable: + \ No newline at end of file diff --git a/test/hydrated_behavior_test.dart b/test/hydrated_behavior_test.dart new file mode 100644 index 0000000..1599de2 --- /dev/null +++ b/test/hydrated_behavior_test.dart @@ -0,0 +1,1103 @@ +// ignore_for_file: close_sinks +import 'dart:async'; + +import 'package:flutter_test/flutter_test.dart'; +import 'package:hydrated/hydrated.dart'; +import 'package:rxdart/rxdart.dart'; + +class StubKeyValueStore implements KeyValueStore { + @override + Future get(String key) { + return Future.value(null); + } + + @override + Future put(String key, T? value) async {} +} + +void main() { + final throwsValueStreamError = throwsA(isA()); + + late StubKeyValueStore mockKeyValueStore; + setUp(() { + mockKeyValueStore = StubKeyValueStore(); + }); + group('HydratedSubject', () { + test('emits the most recently emitted item to every subscriber', () async { + final unseeded = + HydratedSubject('key', persistence: mockKeyValueStore), + seeded = HydratedSubject('key', + seedValue: 0, persistence: mockKeyValueStore); + + unseeded.add(1); + unseeded.add(2); + unseeded.add(3); + + seeded.add(1); + seeded.add(2); + seeded.add(3); + + await expectLater(unseeded.stream, emits(3)); + await expectLater(unseeded.stream, emits(3)); + await expectLater(unseeded.stream, emits(3)); + + await expectLater(seeded.stream, emits(3)); + await expectLater(seeded.stream, emits(3)); + await expectLater(seeded.stream, emits(3)); + }); + + test('emits the most recently emitted null item to every subscriber', + () async { + final unseeded = + HydratedSubject('key', persistence: mockKeyValueStore), + seeded = HydratedSubject('key', + seedValue: 0, persistence: mockKeyValueStore); + + unseeded.add(1); + unseeded.add(2); + unseeded.add(null); + + seeded.add(1); + seeded.add(2); + seeded.add(null); + + await expectLater(unseeded.stream, emits(isNull)); + await expectLater(unseeded.stream, emits(isNull)); + await expectLater(unseeded.stream, emits(isNull)); + + await expectLater(seeded.stream, emits(isNull)); + await expectLater(seeded.stream, emits(isNull)); + await expectLater(seeded.stream, emits(isNull)); + }); + + test( + 'emits the most recently emitted item to every subscriber that subscribe to the subject directly', + () async { + final unseeded = + HydratedSubject('key', persistence: mockKeyValueStore), + seeded = HydratedSubject('key', + seedValue: 0, persistence: mockKeyValueStore); + + unseeded.add(1); + unseeded.add(2); + unseeded.add(3); + + seeded.add(1); + seeded.add(2); + seeded.add(3); + + await expectLater(unseeded, emits(3)); + await expectLater(unseeded, emits(3)); + await expectLater(unseeded, emits(3)); + + await expectLater(seeded, emits(3)); + await expectLater(seeded, emits(3)); + await expectLater(seeded, emits(3)); + }); + + test('emits errors to every subscriber', () async { + final unseeded = + HydratedSubject('key', persistence: mockKeyValueStore), + seeded = HydratedSubject('key', + seedValue: 0, persistence: mockKeyValueStore); + + unseeded.add(1); + unseeded.add(2); + unseeded.add(3); + unseeded.addError(Exception('oh noes!')); + + seeded.add(1); + seeded.add(2); + seeded.add(3); + seeded.addError(Exception('oh noes!')); + + await expectLater(unseeded.stream, emitsError(isException)); + await expectLater(unseeded.stream, emitsError(isException)); + await expectLater(unseeded.stream, emitsError(isException)); + + await expectLater(seeded.stream, emitsError(isException)); + await expectLater(seeded.stream, emitsError(isException)); + await expectLater(seeded.stream, emitsError(isException)); + }); + + test('emits event after error to every subscriber', () async { + final unseeded = + HydratedSubject('key', persistence: mockKeyValueStore), + seeded = HydratedSubject('key', + seedValue: 0, persistence: mockKeyValueStore); + + unseeded.add(1); + unseeded.add(2); + unseeded.addError(Exception('oh noes!')); + unseeded.add(3); + + seeded.add(1); + seeded.add(2); + seeded.addError(Exception('oh noes!')); + seeded.add(3); + + await expectLater(unseeded.stream, emits(3)); + await expectLater(unseeded.stream, emits(3)); + await expectLater(unseeded.stream, emits(3)); + + await expectLater(seeded.stream, emits(3)); + await expectLater(seeded.stream, emits(3)); + await expectLater(seeded.stream, emits(3)); + }); + + test('emits errors to every subscriber', () async { + final unseeded = + HydratedSubject('key', persistence: mockKeyValueStore), + seeded = HydratedSubject('key', + seedValue: 0, persistence: mockKeyValueStore); + final exception = Exception('oh noes!'); + + unseeded.add(1); + unseeded.add(2); + unseeded.add(3); + unseeded.addError(exception); + + seeded.add(1); + seeded.add(2); + seeded.add(3); + seeded.addError(exception); + + expect(unseeded.value, 3); + expect(unseeded.valueOrNull, 3); + expect(unseeded.hasValue, true); + + expect(unseeded.error, exception); + expect(unseeded.errorOrNull, exception); + expect(unseeded.hasError, true); + + await expectLater(unseeded, emitsError(exception)); + await expectLater(unseeded, emitsError(exception)); + await expectLater(unseeded, emitsError(exception)); + + expect(seeded.value, 3); + expect(seeded.valueOrNull, 3); + expect(seeded.hasValue, true); + + expect(seeded.error, exception); + expect(seeded.errorOrNull, exception); + expect(seeded.hasError, true); + + await expectLater(seeded, emitsError(exception)); + await expectLater(seeded, emitsError(exception)); + await expectLater(seeded, emitsError(exception)); + }); + + test('can synchronously get the latest value', () { + final unseeded = + HydratedSubject('key', persistence: mockKeyValueStore), + seeded = HydratedSubject('key', + seedValue: 0, persistence: mockKeyValueStore); + + unseeded.add(1); + unseeded.add(2); + unseeded.add(3); + + seeded.add(1); + seeded.add(2); + seeded.add(3); + + expect(unseeded.value, 3); + expect(unseeded.valueOrNull, 3); + expect(unseeded.hasValue, true); + + expect(seeded.value, 3); + expect(seeded.valueOrNull, 3); + expect(seeded.hasValue, true); + }); + + test('can synchronously get the latest null value', () async { + final unseeded = + HydratedSubject('key', persistence: mockKeyValueStore), + seeded = HydratedSubject('key', + seedValue: 0, persistence: mockKeyValueStore); + + unseeded.add(1); + unseeded.add(2); + unseeded.add(null); + + seeded.add(1); + seeded.add(2); + seeded.add(null); + + expect(unseeded.value, isNull); + expect(unseeded.valueOrNull, isNull); + expect(unseeded.hasValue, true); + + expect(seeded.value, isNull); + expect(seeded.valueOrNull, isNull); + expect(seeded.hasValue, true); + }); + + test('emits the seed item if no new items have been emitted', () async { + final subject = HydratedSubject('key', + seedValue: 1, persistence: mockKeyValueStore); + + await expectLater(subject.stream, emits(1)); + await expectLater(subject.stream, emits(1)); + await expectLater(subject.stream, emits(1)); + }); + + test('can synchronously get the initial value', () { + final subject = HydratedSubject('key', + seedValue: 1, persistence: mockKeyValueStore); + + expect(subject.value, 1); + expect(subject.valueOrNull, 1); + expect(subject.hasValue, true); + }); + + test('cannot synchronously get the initial null value', () { + final subject = HydratedSubject('key', + seedValue: null, persistence: mockKeyValueStore); + + expect(subject.hasValue, false); + expect(subject.valueOrNull, null); + }); + + test('initial value is null when no value has been emitted', () { + final subject = + HydratedSubject('key', persistence: mockKeyValueStore); + + expect(() => subject.value, throwsValueStreamError); + expect(subject.valueOrNull, null); + expect(subject.hasValue, false); + }); + + test('emits done event to listeners when the subject is closed', () async { + final unseeded = + HydratedSubject('key', persistence: mockKeyValueStore), + seeded = HydratedSubject('key', + seedValue: 0, persistence: mockKeyValueStore); + + await expectLater(unseeded.isClosed, isFalse); + await expectLater(seeded.isClosed, isFalse); + + unseeded.add(1); + scheduleMicrotask(() => unseeded.close()); + + seeded.add(1); + scheduleMicrotask(() => seeded.close()); + + await expectLater(unseeded.stream, emitsInOrder([1, emitsDone])); + await expectLater(unseeded.isClosed, isTrue); + + await expectLater(seeded.stream, emitsInOrder([1, emitsDone])); + await expectLater(seeded.isClosed, isTrue); + }); + + test('emits error events to subscribers', () async { + final unseeded = + HydratedSubject('key', persistence: mockKeyValueStore), + seeded = HydratedSubject('key', + seedValue: 0, persistence: mockKeyValueStore); + + scheduleMicrotask(() => unseeded.addError(Exception())); + scheduleMicrotask(() => seeded.addError(Exception())); + + await expectLater(unseeded.stream, emitsError(isException)); + await expectLater(seeded.stream, emitsError(isException)); + }); + + test('replays the previously emitted items from addStream', () async { + final unseeded = + HydratedSubject('key', persistence: mockKeyValueStore), + seeded = HydratedSubject('key', + seedValue: 0, persistence: mockKeyValueStore); + + await unseeded.addStream(Stream.fromIterable(const [1, 2, 3])); + await seeded.addStream(Stream.fromIterable(const [1, 2, 3])); + + await expectLater(unseeded.stream, emits(3)); + await expectLater(unseeded.stream, emits(3)); + await expectLater(unseeded.stream, emits(3)); + + await expectLater(seeded.stream, emits(3)); + await expectLater(seeded.stream, emits(3)); + await expectLater(seeded.stream, emits(3)); + }); + + test('replays the previously emitted errors from addStream', () async { + final unseeded = + HydratedSubject('key', persistence: mockKeyValueStore), + seeded = HydratedSubject('key', + seedValue: 0, persistence: mockKeyValueStore); + + await unseeded.addStream(Stream.error('error'), + cancelOnError: false); + await seeded.addStream(Stream.error('error'), cancelOnError: false); + + await expectLater(unseeded.stream, emitsError('error')); + await expectLater(unseeded.stream, emitsError('error')); + }); + + test('allows items to be added once addStream is complete', () async { + final subject = + HydratedSubject('key', persistence: mockKeyValueStore); + + await subject.addStream(Stream.fromIterable(const [1, 2])); + subject.add(3); + + await expectLater(subject.stream, emits(3)); + }); + + test('allows items to be added once addStream completes with an error', + () async { + final subject = + HydratedSubject('key', persistence: mockKeyValueStore); + + subject + .addStream(Stream.error(Exception()), cancelOnError: true) + .whenComplete(() => subject.add(1)); + + await expectLater(subject.stream, + emitsInOrder([emitsError(isException), emits(1)])); + }); + + test('does not allow events to be added when addStream is active', + () async { + final subject = + HydratedSubject('key', persistence: mockKeyValueStore); + + // Purposely don't wait for the future to complete, then try to add items + + subject.addStream(Stream.fromIterable(const [1, 2, 3])); + + await expectLater(() => subject.add(1), throwsStateError); + }); + + test('does not allow errors to be added when addStream is active', + () async { + final subject = + HydratedSubject('key', persistence: mockKeyValueStore); + + // Purposely don't wait for the future to complete, then try to add items + + subject.addStream(Stream.fromIterable(const [1, 2, 3])); + + await expectLater(() => subject.addError(Error()), throwsStateError); + }); + + test('does not allow subject to be closed when addStream is active', + () async { + final subject = + HydratedSubject('key', persistence: mockKeyValueStore); + + // Purposely don't wait for the future to complete, then try to add items + + subject.addStream(Stream.fromIterable(const [1, 2, 3])); + + await expectLater(() => subject.close(), throwsStateError); + }); + + test( + 'does not allow addStream to add items when previous addStream is active', + () async { + final subject = + HydratedSubject('key', persistence: mockKeyValueStore); + + // Purposely don't wait for the future to complete, then try to add items + + subject.addStream(Stream.fromIterable(const [1, 2, 3])); + + await expectLater(() => subject.addStream(Stream.fromIterable(const [1])), + throwsStateError); + }); + + test('returns onListen callback set in constructor', () async { + final testOnListen = () {}; + + final subject = HydratedSubject('key', + onListen: testOnListen, persistence: mockKeyValueStore); + + await expectLater(subject.onListen, testOnListen); + }); + + test('sets onListen callback', () async { + final testOnListen = () {}; + + final subject = + HydratedSubject('key', persistence: mockKeyValueStore); + + await expectLater(subject.onListen, isNull); + + subject.onListen = testOnListen; + + await expectLater(subject.onListen, testOnListen); + }); + + test('returns onCancel callback set in constructor', () async { + final onCancel = () => Future.value(null); + + final subject = HydratedSubject('key', + onCancel: onCancel, persistence: mockKeyValueStore); + + await expectLater(subject.onCancel, onCancel); + }); + + test('sets onCancel callback', () async { + final testOnCancel = () {}; + + final subject = + HydratedSubject('key', persistence: mockKeyValueStore); + + await expectLater(subject.onCancel, isNull); + + subject.onCancel = testOnCancel; + + await expectLater(subject.onCancel, testOnCancel); + }); + + test('reports if a listener is present', () async { + final subject = + HydratedSubject('key', persistence: mockKeyValueStore); + + await expectLater(subject.hasListener, isFalse); + + subject.stream.listen(null); + + await expectLater(subject.hasListener, isTrue); + }); + + test('onPause unsupported', () { + final subject = + HydratedSubject('key', persistence: mockKeyValueStore); + + expect(subject.isPaused, isFalse); + expect(() => subject.onPause, throwsUnsupportedError); + expect(() => subject.onPause = () {}, throwsUnsupportedError); + }); + + test('onResume unsupported', () { + final subject = + HydratedSubject('key', persistence: mockKeyValueStore); + + expect(() => subject.onResume, throwsUnsupportedError); + expect(() => subject.onResume = () {}, throwsUnsupportedError); + }); + + test('returns controller sink', () async { + final subject = + HydratedSubject('key', persistence: mockKeyValueStore); + + await expectLater(subject.sink, TypeMatcher>()); + }); + + test('correctly closes done Future', () async { + final subject = + HydratedSubject('key', persistence: mockKeyValueStore); + + scheduleMicrotask(() => subject.close()); + + await expectLater(subject.done, completes); + }); + + test('can be listened to multiple times', () async { + final subject = + HydratedSubject('key', seedValue: 1, persistence: mockKeyValueStore); + final stream = subject.stream; + + await expectLater(stream, emits(1)); + await expectLater(stream, emits(1)); + }); + + test('always returns the same stream', () async { + final subject = + HydratedSubject('key', persistence: mockKeyValueStore); + + await expectLater(subject.stream, equals(subject.stream)); + }); + + test('adding to sink has same behavior as adding to Subject itself', + () async { + final subject = + HydratedSubject('key', persistence: mockKeyValueStore); + + subject.sink.add(1); + + expect(subject.value, 1); + + subject.sink.add(2); + subject.sink.add(3); + + await expectLater(subject.stream, emits(3)); + await expectLater(subject.stream, emits(3)); + await expectLater(subject.stream, emits(3)); + }); + + test('setter `value=` has same behavior as adding to Subject', () async { + final subject = + HydratedSubject('key', seedValue: 0, persistence: mockKeyValueStore); + + subject.value = 1; + + // await pumpEventQueue(); + + expect(subject.value, 1); + + subject.value = 2; + subject.value = 3; + // await pumpEventQueue(); + + await expectLater(subject.stream, emits(3)); + await expectLater(subject.stream, emits(3)); + await expectLater(subject.stream, emits(3)); + }); + + test('is always treated as a broadcast Stream', () async { + final subject = + HydratedSubject('key', persistence: mockKeyValueStore); + final stream = subject.asyncMap((event) => Future.value(event)); + + expect(subject.isBroadcast, isTrue); + expect(stream.isBroadcast, isTrue); + }); + + test('hasValue returns false for an empty subject', () { + final subject = + HydratedSubject('key', persistence: mockKeyValueStore); + + expect(subject.hasValue, isFalse); + }); + + test('hasValue returns true for a seeded subject with non-null seed', () { + final subject = HydratedSubject('key', + seedValue: 1, persistence: mockKeyValueStore); + + expect(subject.hasValue, isTrue); + }); + + test('hasValue returns false for a seeded subject with null seed', () { + final subject = HydratedSubject('key', + seedValue: null, persistence: mockKeyValueStore); + + expect(subject.hasValue, isFalse); + }); + + test('hasValue returns true for an unseeded subject after an emission', () { + final subject = + HydratedSubject('key', persistence: mockKeyValueStore); + + subject.add(1); + + expect(subject.hasValue, isTrue); + }); + + test('hasError returns false for an empty subject', () { + final subject = + HydratedSubject('key', persistence: mockKeyValueStore); + + expect(subject.hasError, isFalse); + }); + + test('hasError returns false for a seeded subject with non-null seed', () { + final subject = HydratedSubject('key', + seedValue: 1, persistence: mockKeyValueStore); + + expect(subject.hasError, isFalse); + }); + + test('hasError returns false for a seeded subject with null seed', () { + final subject = HydratedSubject('key', + seedValue: null, persistence: mockKeyValueStore); + + expect(subject.hasError, isFalse); + }); + + test('hasError returns false for an unseeded subject after an emission', + () { + final subject = + HydratedSubject('key', persistence: mockKeyValueStore); + + subject.add(1); + + expect(subject.hasError, isFalse); + }); + + test('hasError returns true for an unseeded subject after addError', () { + final subject = + HydratedSubject('key', persistence: mockKeyValueStore); + + subject.add(1); + subject.addError('error'); + + expect(subject.hasError, isTrue); + }); + + test('hasError returns true for a seeded subject after addError', () { + final subject = HydratedSubject('key', + seedValue: 1, persistence: mockKeyValueStore); + + subject.addError('error'); + + expect(subject.hasError, isTrue); + }); + + test('error returns null for an empty subject', () { + final subject = + HydratedSubject('key', persistence: mockKeyValueStore); + + expect(subject.hasError, isFalse); + expect(subject.errorOrNull, isNull); + expect(() => subject.error, throwsValueStreamError); + }); + + test('error returns null for a seeded subject with non-null seed', () { + final subject = HydratedSubject('key', + seedValue: 1, persistence: mockKeyValueStore); + + expect(subject.hasError, isFalse); + expect(subject.errorOrNull, isNull); + expect(() => subject.error, throwsValueStreamError); + }); + + test('error returns null for a seeded subject with null seed', () { + final subject = HydratedSubject('key', + seedValue: null, persistence: mockKeyValueStore); + + expect(subject.hasError, isFalse); + expect(subject.errorOrNull, isNull); + expect(() => subject.error, throwsValueStreamError); + }); + + test('can synchronously get the latest error', () async { + final unseeded = + HydratedSubject('key', persistence: mockKeyValueStore), + seeded = HydratedSubject('key', + seedValue: 0, persistence: mockKeyValueStore); + + unseeded.add(1); + unseeded.add(2); + unseeded.add(3); + expect(unseeded.hasError, isFalse); + expect(unseeded.errorOrNull, isNull); + expect(() => unseeded.error, throwsValueStreamError); + + unseeded.addError(Exception('oh noes!')); + expect(unseeded.hasError, isTrue); + expect(unseeded.errorOrNull, isException); + expect(unseeded.error, isException); + + seeded.add(1); + seeded.add(2); + seeded.add(3); + expect(seeded.hasError, isFalse); + expect(seeded.errorOrNull, isNull); + expect(() => seeded.error, throwsValueStreamError); + + seeded.addError(Exception('oh noes!')); + expect(seeded.hasError, isTrue); + expect(seeded.errorOrNull, isException); + expect(seeded.error, isException); + }); + + test('emits event after error to every subscriber', () async { + final unseeded = + HydratedSubject('key', persistence: mockKeyValueStore), + seeded = HydratedSubject('key', + seedValue: 0, persistence: mockKeyValueStore); + + unseeded.add(1); + unseeded.add(2); + unseeded.addError(Exception('oh noes!')); + expect(unseeded.hasError, isTrue); + expect(unseeded.errorOrNull, isException); + expect(unseeded.error, isException); + unseeded.add(3); + expect(unseeded.hasError, isTrue); + expect(unseeded.errorOrNull, isException); + expect(unseeded.error, isException); + + seeded.add(1); + seeded.add(2); + seeded.addError(Exception('oh noes!')); + expect(seeded.hasError, isTrue); + expect(seeded.errorOrNull, isException); + expect(seeded.error, isException); + seeded.add(3); + expect(seeded.hasError, isTrue); + expect(seeded.errorOrNull, isException); + expect(seeded.error, isException); + }); + group('override built-in', () { + test('where', () { + { + var hydratedSubject = HydratedSubject('key', seedValue: 1, persistence: mockKeyValueStore); + + var stream = hydratedSubject.where((event) => event.isOdd); + expect(stream, emitsInOrder([1, 3])); + + hydratedSubject.add(2); + hydratedSubject.add(3); + } + + { + var hydratedSubject = + HydratedSubject('key', persistence: mockKeyValueStore); + + var stream = hydratedSubject.where((event) => event?.isOdd ?? false); + expect(stream, emitsInOrder([1, 3])); + + hydratedSubject.add(1); + hydratedSubject.add(2); + hydratedSubject.add(3); + } + }); + + test('map', () { + { + var hydratedSubject = HydratedSubject('key', seedValue: 1, persistence: mockKeyValueStore); + + var mapped = hydratedSubject.map((event) => event + 1); + expect(mapped, emitsInOrder([2, 3])); + + hydratedSubject.add(2); + } + + { + var hydratedSubject = + HydratedSubject('key', persistence: mockKeyValueStore); + + var mapped = hydratedSubject.map((event) => (event ?? 0) + 1); + expect(mapped, emitsInOrder([2, 3])); + + hydratedSubject.add(1); + hydratedSubject.add(2); + } + }); + + test('asyncMap', () { + { + var hydratedSubject = HydratedSubject('key', seedValue: 1, persistence: mockKeyValueStore); + + var mapped = + hydratedSubject.asyncMap((event) => Future.value(event + 1)); + expect(mapped, emitsInOrder([2, 3])); + + hydratedSubject.add(2); + } + + { + var hydratedSubject = + HydratedSubject('key', persistence: mockKeyValueStore); + + var mapped = hydratedSubject + .asyncMap((event) => Future.value((event ?? 0) + 1)); + expect(mapped, emitsInOrder([2, 3])); + + hydratedSubject.add(1); + hydratedSubject.add(2); + } + }); + + test('asyncExpand', () { + { + var hydratedSubject = HydratedSubject('key', seedValue: 1, persistence: mockKeyValueStore); + + var stream = + hydratedSubject.asyncExpand((event) => Stream.value(event + 1)); + expect(stream, emitsInOrder([2, 3])); + + hydratedSubject.add(2); + } + + { + var hydratedSubject = + HydratedSubject('key', persistence: mockKeyValueStore); + + var stream = + hydratedSubject.asyncExpand((event) => Stream.value(event! + 1)); + expect(stream, emitsInOrder([2, 3])); + + hydratedSubject.add(1); + hydratedSubject.add(2); + } + }); + + test('handleError', () { + { + var hydratedSubject = HydratedSubject('key', seedValue: 1, persistence: mockKeyValueStore); + + var stream = hydratedSubject.handleError( + expectAsync1( + (dynamic e) => expect(e, isException), + count: 1, + ), + ); + + expect( + stream, + emitsInOrder([1, 2]), + ); + + hydratedSubject.addError(Exception()); + hydratedSubject.add(2); + } + + { + var hydratedSubject = + HydratedSubject('key', persistence: mockKeyValueStore); + + var stream = hydratedSubject.handleError( + expectAsync1( + (dynamic e) => expect(e, isException), + count: 1, + ), + ); + + expect( + stream, + emitsInOrder([1, 2]), + ); + + hydratedSubject.add(1); + hydratedSubject.addError(Exception()); + hydratedSubject.add(2); + } + }); + + test('expand', () { + { + var hydratedSubject = HydratedSubject('key', seedValue: 1, persistence: mockKeyValueStore); + + var stream = hydratedSubject.expand((event) => [event + 1]); + expect(stream, emitsInOrder([2, 3])); + + hydratedSubject.add(2); + } + + { + var hydratedSubject = + HydratedSubject('key', persistence: mockKeyValueStore); + + var stream = hydratedSubject.expand((event) => [event! + 1]); + expect(stream, emitsInOrder([2, 3])); + + hydratedSubject.add(1); + hydratedSubject.add(2); + } + }); + + test('transform', () { + { + var hydratedSubject = HydratedSubject('key', seedValue: 1, persistence: mockKeyValueStore); + + var stream = hydratedSubject.transform( + IntervalStreamTransformer(const Duration(milliseconds: 100))); + expect(stream, emitsInOrder([1, 2])); + + hydratedSubject.add(2); + } + + { + var hydratedSubject = + HydratedSubject('key', persistence: mockKeyValueStore); + + var stream = hydratedSubject.transform( + IntervalStreamTransformer(const Duration(milliseconds: 100))); + expect(stream, emitsInOrder([1, 2])); + + hydratedSubject.add(1); + hydratedSubject.add(2); + } + }); + + test('cast', () { + { + var hydratedSubject = HydratedSubject('key', + seedValue: 1, persistence: mockKeyValueStore); + + var stream = hydratedSubject.cast(); + expect(stream, emitsInOrder([1, 2])); + + hydratedSubject.add(2); + } + + { + var hydratedSubject = + HydratedSubject('key', persistence: mockKeyValueStore); + + var stream = hydratedSubject.cast(); + expect(stream, emitsInOrder([1, 2])); + + hydratedSubject.add(1); + hydratedSubject.add(2); + } + }); + + test('take', () { + { + var hydratedSubject = HydratedSubject('key', seedValue: 1, persistence: mockKeyValueStore); + + var stream = hydratedSubject.take(2); + expect(stream, emitsInOrder([1, 2])); + + hydratedSubject.add(2); + hydratedSubject.add(3); + } + + { + var hydratedSubject = + HydratedSubject('key', persistence: mockKeyValueStore); + + var stream = hydratedSubject.take(2); + expect(stream, emitsInOrder([1, 2])); + + hydratedSubject.add(1); + hydratedSubject.add(2); + hydratedSubject.add(3); + } + }); + + test('takeWhile', () { + { + var hydratedSubject = HydratedSubject('key', seedValue: 1, persistence: mockKeyValueStore); + + var stream = hydratedSubject.takeWhile((element) => element <= 2); + expect(stream, emitsInOrder([1, 2])); + + hydratedSubject.add(2); + hydratedSubject.add(3); + } + + { + var hydratedSubject = + HydratedSubject('key', persistence: mockKeyValueStore); + + var stream = hydratedSubject.takeWhile((element) => element! <= 2); + expect(stream, emitsInOrder([1, 2])); + + hydratedSubject.add(1); + hydratedSubject.add(2); + hydratedSubject.add(3); + } + }); + + test('skip', () { + { + var hydratedSubject = HydratedSubject('key', seedValue: 1, persistence: mockKeyValueStore); + + var stream = hydratedSubject.skip(2); + expect(stream, emitsInOrder([3, 4])); + + hydratedSubject.add(2); + hydratedSubject.add(3); + hydratedSubject.add(4); + } + + { + var hydratedSubject = + HydratedSubject('key', persistence: mockKeyValueStore); + + var stream = hydratedSubject.skip(2); + expect(stream, emitsInOrder([3, 4])); + + hydratedSubject.add(1); + hydratedSubject.add(2); + hydratedSubject.add(3); + hydratedSubject.add(4); + } + }); + + test('skipWhile', () { + { + var hydratedSubject = HydratedSubject('key', seedValue: 1, persistence: mockKeyValueStore); + + var stream = hydratedSubject.skipWhile((element) => element < 3); + expect(stream, emitsInOrder([3, 4])); + + hydratedSubject.add(2); + hydratedSubject.add(3); + hydratedSubject.add(4); + } + + { + var hydratedSubject = + HydratedSubject('key', persistence: mockKeyValueStore); + + var stream = hydratedSubject.skipWhile((element) => element! < 3); + expect(stream, emitsInOrder([3, 4])); + + hydratedSubject.add(1); + hydratedSubject.add(2); + hydratedSubject.add(3); + hydratedSubject.add(4); + } + }); + + test('distinct', () { + { + var hydratedSubject = HydratedSubject('key', seedValue: 1, persistence: mockKeyValueStore); + + var stream = hydratedSubject.distinct(); + expect(stream, emitsInOrder([1, 2])); + + hydratedSubject.add(1); + hydratedSubject.add(2); + hydratedSubject.add(2); + } + + { + var hydratedSubject = + HydratedSubject('key', persistence: mockKeyValueStore); + + var stream = hydratedSubject.distinct(); + expect(stream, emitsInOrder([1, 2])); + + hydratedSubject.add(1); + hydratedSubject.add(1); + hydratedSubject.add(2); + hydratedSubject.add(2); + } + }); + + test('timeout', () { + { + var hydratedSubject = HydratedSubject('key', seedValue: 1, persistence: mockKeyValueStore); + + var stream = hydratedSubject + .interval(const Duration(milliseconds: 100)) + .timeout( + const Duration(milliseconds: 70), + onTimeout: expectAsync1( + (EventSink sink) {}, + count: 4, + ), + ); + + expect(stream, emitsInOrder([1, 2, 3, 4])); + + hydratedSubject.add(2); + hydratedSubject.add(3); + hydratedSubject.add(4); + } + + { + var hydratedSubject = + HydratedSubject('key', persistence: mockKeyValueStore); + + var stream = hydratedSubject + .interval(const Duration(milliseconds: 100)) + .timeout( + const Duration(milliseconds: 70), + onTimeout: expectAsync1( + (EventSink sink) {}, + count: 4, + ), + ); + + expect(stream, emitsInOrder([1, 2, 3, 4])); + + hydratedSubject.add(1); + hydratedSubject.add(2); + hydratedSubject.add(3); + hydratedSubject.add(4); + } + }); + }); + }); +} From e5910c9b8e75da86df605aaed803b1fe8b4800b6 Mon Sep 17 00:00:00 2001 From: Yurii Prykhodko Date: Mon, 23 Aug 2021 21:34:42 +0300 Subject: [PATCH 12/21] Add HydratedSubject tests (WIP) --- lib/src/hydrated.dart | 4 ++ test/hydrated_test.dart | 150 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 154 insertions(+) create mode 100644 test/hydrated_test.dart diff --git a/lib/src/hydrated.dart b/lib/src/hydrated.dart index d3c5ce8..b14dab1 100644 --- a/lib/src/hydrated.dart +++ b/lib/src/hydrated.dart @@ -83,6 +83,10 @@ class HydratedSubject extends Subject implements ValueStream { bool sync = false, KeyValueStore persistence = const SharedPreferencesPersistence(), }) { + assert( + (hydrate == null && persist == null) || + (hydrate != null && persist != null), + '`hydrate` and `persist` callbacks must both be present.'); // ignore: close_sinks final subject = seedValue != null ? BehaviorSubject.seeded( diff --git a/test/hydrated_test.dart b/test/hydrated_test.dart new file mode 100644 index 0000000..d5a3068 --- /dev/null +++ b/test/hydrated_test.dart @@ -0,0 +1,150 @@ +// ignore_for_file: close_sinks +import 'package:flutter_test/flutter_test.dart'; +import 'package:hydrated/hydrated.dart'; + +typedef _GetOverride = Future Function(String key); +typedef _PutOverride = Future Function(String key, dynamic value); + +class _InMemoryKeyValueStore implements KeyValueStore { + final Map store = {}; + + _GetOverride? getOverride; + @override + Future get(String key) async { + if (getOverride != null) return getOverride!(key) as Future; + return store[key] as T?; + } + + _PutOverride? putOverride; + @override + Future put(String key, T? value) async { + if (putOverride != null) { + putOverride!(key, value); + return; + } + store[key] = value; + } +} + +class TestDataClass { + final int value; + + TestDataClass(this.value); + + static TestDataClass fromJson(Map json) => + TestDataClass(json['value'] as int); + + Map toJson() => {'value': value}; +} + +void main() { + const key = 'key'; + late _InMemoryKeyValueStore mockKeyValueStore; + setUp(() { + mockKeyValueStore = _InMemoryKeyValueStore(); + }); + + group('HydratedSubject', () { + group('hydration', () { + test('Tries to hydrate upon instantiation', () { + mockKeyValueStore.getOverride = expectAsync1((_) async {}, count: 1); + + HydratedSubject(key, persistence: mockKeyValueStore); + }); + + test( + 'Given persisted value is present, when it hydrates, it emits the value', + () { + mockKeyValueStore.getOverride = (_) async => 42; + final subject = + HydratedSubject(key, persistence: mockKeyValueStore); + + expect(subject, emits(42)); + }); + + test('Given persisted value is null, when it hydrates, it emits nothing', + () { + mockKeyValueStore.getOverride = (_) async => null; + final subject = + HydratedSubject(key, persistence: mockKeyValueStore); + + expect(subject, neverEmits(anything)); + subject.close(); + }); + + test( + 'Given `hydrate` is supplied, but `persist` is ommited, it throws an AssertionError', + () { + expect(() { + HydratedSubject(key, + persistence: mockKeyValueStore, hydrate: (_) => 1); + }, throwsA(isA())); + }); + + test( + 'Given `persist` is supplied, but `hydrate` is ommited, it throws an AssertionError', + () { + expect(() { + HydratedSubject(key, + persistence: mockKeyValueStore, persist: (_) => ''); + }, throwsA(isA())); + }); + + test('uses hydrate callback, and emits the output of the callback', () { + const testPersistedValue = '24'; + const testHydratedValue = 42; + mockKeyValueStore.getOverride = (_) async => testPersistedValue; + + final hydrateCallback = expectAsync1((String persistedValue) { + expect(persistedValue, equals(testPersistedValue)); + return testHydratedValue; + }, count: 1); + + final subject = HydratedSubject( + key, + persistence: mockKeyValueStore, + hydrate: hydrateCallback, + persist: (_) => '', + ); + + expect(subject, emits(testHydratedValue)); + }); + }); + + test('when adding a value, it saves the value with the key-value store', + () { + const testValue = 42; + mockKeyValueStore.putOverride = expectAsync2((key, value) async { + expect(value, equals(testValue)); + }, count: 1); + final subject = HydratedSubject(key, persistence: mockKeyValueStore); + + subject.add(testValue); + }); + + test( + 'when adding a value, it uses the `persist` callback, and saves the output of this callback', + () { + const testAddedValue = 42; + const testPersistedValue = '24'; + + final persistCallback = expectAsync1((int value) { + expect(value, equals(testAddedValue)); + return testPersistedValue; + }, count: 1); + + mockKeyValueStore.putOverride = expectAsync2((key, value) async { + expect(value, isA()); + expect(value, equals(testPersistedValue)); + }, count: 1); + final subject = HydratedSubject( + key, + persistence: mockKeyValueStore, + hydrate: (_) => 1, + persist: persistCallback, + ); + + subject.add(testAddedValue); + }); + }); +} From 2c0b827976aab223235f2db3ecc157703baf4cc9 Mon Sep 17 00:00:00 2001 From: Yurii Prykhodko Date: Thu, 26 Aug 2021 12:37:06 +0300 Subject: [PATCH 13/21] Remove the use of `!` ("bang") operator --- lib/src/hydrated.dart | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/lib/src/hydrated.dart b/lib/src/hydrated.dart index b14dab1..fbfe4b3 100644 --- a/lib/src/hydrated.dart +++ b/lib/src/hydrated.dart @@ -151,10 +151,11 @@ class HydratedSubject extends Subject implements ValueStream { Future _hydrateSubject() async { try { T? val; - if (_hydrate != null) { + final hydrate = _hydrate; + if (hydrate != null) { final persistedValue = await _persistence.get(_key); if (persistedValue != null) { - val = _hydrate!(persistedValue); + val = hydrate(persistedValue); } } else { val = await _persistence.get(_key); @@ -174,9 +175,10 @@ class HydratedSubject extends Subject implements ValueStream { void _persistValue(T val) async { try { + final persist = _persist; var persistedVal; - if (_persist != null) { - persistedVal = _persist!(val); + if (persist != null) { + persistedVal = persist(val); _persistence.put(_key, persistedVal); } else { persistedVal = val; From d616400160f3812b0e5749b34309fc906ae2b11a Mon Sep 17 00:00:00 2001 From: Yurii Prykhodko Date: Thu, 26 Aug 2021 13:56:29 +0300 Subject: [PATCH 14/21] More unit-tests for HydratedSubject --- lib/src/hydrated.dart | 4 +-- test/hydrated_test.dart | 62 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 64 insertions(+), 2 deletions(-) diff --git a/lib/src/hydrated.dart b/lib/src/hydrated.dart index fbfe4b3..45a789c 100644 --- a/lib/src/hydrated.dart +++ b/lib/src/hydrated.dart @@ -179,10 +179,10 @@ class HydratedSubject extends Subject implements ValueStream { var persistedVal; if (persist != null) { persistedVal = persist(val); - _persistence.put(_key, persistedVal); + await _persistence.put(_key, persistedVal); } else { persistedVal = val; - _persistence.put(_key, persistedVal); + await _persistence.put(_key, persistedVal); } } on PersistenceError catch (e, s) { addError(e, s); diff --git a/test/hydrated_test.dart b/test/hydrated_test.dart index d5a3068..cf94609 100644 --- a/test/hydrated_test.dart +++ b/test/hydrated_test.dart @@ -1,4 +1,6 @@ // ignore_for_file: close_sinks +import 'dart:async'; + import 'package:flutter_test/flutter_test.dart'; import 'package:hydrated/hydrated.dart'; @@ -111,6 +113,12 @@ void main() { }); }); + test('exposes the persistence key', () { + final subject = HydratedSubject(key, persistence: mockKeyValueStore); + + expect(subject.key, key); + }); + test('when adding a value, it saves the value with the key-value store', () { const testValue = 42; @@ -146,5 +154,59 @@ void main() { subject.add(testAddedValue); }); + + group('persistence error handling', () { + test( + 'given persistence interface `get` throws a PersistenceError, ' + 'it emits the error through the stream', () { + mockKeyValueStore.getOverride = + (_) async => throw PersistenceError('test'); + final subject = + HydratedSubject(key, persistence: mockKeyValueStore); + + expect(subject, emitsError(isA())); + }); + + test( + 'given persistence interface `get` throws an Exception, ' + 'constructing the HydratedSubject throws an asynchronous uncatchable error', + () { + mockKeyValueStore.getOverride = (_) async => throw Exception('test'); + runZonedGuarded( + () { + final completer = Completer(); + HydratedSubject( + key, + persistence: mockKeyValueStore, + onHydrate: completer.complete, + ); + return completer.future; + }, + expectAsync2((error, _) { + expect(error, isA()); + }, count: 1), + ); + }); + + test( + 'AAAA given persistence interface put throws a PersistenceError, ' + 'it emits the error through the stream', () async { + const testValue = 42; + mockKeyValueStore.putOverride = + (_, __) => throw PersistenceError('test'); + final subject = + HydratedSubject(key, persistence: mockKeyValueStore); + + final expectation = expectLater( + subject, + emitsInOrder([ + 42, + emitsError(isA()), + ])); + subject.add(testValue); + + await expectation; + }); + }); }); } From 7c45d85158ac3b38ff3426810caaa05310fd6724 Mon Sep 17 00:00:00 2001 From: Yurii Prykhodko Date: Thu, 26 Aug 2021 14:14:20 +0300 Subject: [PATCH 15/21] More persistence unit-tests --- .../shared_preferences_persistence.dart | 37 +++++++--------- test/shared_preferences_persistence_test.dart | 43 ++++++++++++++++++- 2 files changed, 58 insertions(+), 22 deletions(-) diff --git a/lib/src/persistence/shared_preferences_persistence.dart b/lib/src/persistence/shared_preferences_persistence.dart index 71183c8..f62ea06 100644 --- a/lib/src/persistence/shared_preferences_persistence.dart +++ b/lib/src/persistence/shared_preferences_persistence.dart @@ -24,21 +24,23 @@ class SharedPreferencesPersistence implements KeyValueStore { T? val; - if (_areTypesEqual() || _areTypesEqual()) - val = prefs.getInt(key) as T?; - else if (_areTypesEqual() || _areTypesEqual()) - val = prefs.getDouble(key) as T?; - else if (_areTypesEqual() || _areTypesEqual()) - val = prefs.getBool(key) as T?; - else if (_areTypesEqual() || _areTypesEqual()) - val = prefs.getString(key) as T?; - else if (_areTypesEqual>() || - _areTypesEqual?>()) - val = prefs.getStringList(key) as T?; - else + try { + if (_areTypesEqual() || _areTypesEqual()) + val = prefs.getInt(key) as T?; + else if (_areTypesEqual() || _areTypesEqual()) + val = prefs.getDouble(key) as T?; + else if (_areTypesEqual() || _areTypesEqual()) + val = prefs.getBool(key) as T?; + else if (_areTypesEqual() || _areTypesEqual()) + val = prefs.getString(key) as T?; + else if (_areTypesEqual>() || + _areTypesEqual?>()) + val = prefs.getStringList(key) as T?; + } catch (e) { throw PersistenceError( - 'Shared Preferences returned an invalid type', + 'Error retrieving value from SharedPreferences: $e', ); + } return val; } @@ -58,14 +60,7 @@ class SharedPreferencesPersistence implements KeyValueStore { await prefs.setBool(key, value); else if (value is String) await prefs.setString(key, value); - else if (value is List) - await prefs.setStringList(key, value); - else { - throw PersistenceError( - 'HydratedSubject – value must be int, ' - 'double, bool, String, or List', - ); - } + else if (value is List) await prefs.setStringList(key, value); } void _assertSupportedType() { diff --git a/test/shared_preferences_persistence_test.dart b/test/shared_preferences_persistence_test.dart index 690b427..0dba899 100644 --- a/test/shared_preferences_persistence_test.dart +++ b/test/shared_preferences_persistence_test.dart @@ -20,7 +20,48 @@ void main() { expect(value, isTrue); }); - group('HydratedSubject', () { + group('SharedPreferencesPersistence', () { + group('handles unsupported types', () { + test( + 'when saving a value with an unsupported type, it throws an AssertionError', + () { + final unsupportedTypeValue = Exception('test unsupported value'); + expect( + () => SharedPreferencesPersistence().put('key', unsupportedTypeValue), + throwsA(isA()), + ); + }); + + test( + 'when getting a value with an unspecified type (dynamic), it throws an AssertionError', + () { + expect( + () => SharedPreferencesPersistence().get('key'), + throwsA(isA()), + ); + }); + + test( + 'when getting a value with an unsupported type, it throws an AssertionError', + () { + expect( + () => SharedPreferencesPersistence().get('key'), + throwsA(isA()), + ); + }); + + test( + 'when SharedPreferences return an unsupported type, it throws a PersistenceError', + () { + final unsupportedTypeValue = Exception('test unsupported value'); + _setMockPersistedValue('key', unsupportedTypeValue); + expect( + () => SharedPreferencesPersistence().get('key'), + throwsA(isA()), + ); + }); + }); + group('correctly handles data type', () { test('int', () async { await _testPersistence("int", 1, 2); From 6d6926379a41d4e049e6bed2d27d01958016c336 Mon Sep 17 00:00:00 2001 From: Yurii Prykhodko Date: Thu, 26 Aug 2021 14:15:00 +0300 Subject: [PATCH 16/21] Ignore VSCode files and coverage dir --- .gitignore | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index a247422..234b406 100644 --- a/.gitignore +++ b/.gitignore @@ -18,7 +18,7 @@ # The .vscode folder contains launch configuration and tasks you configure in # VS Code which you may wish to be included in version control, so this line # is commented out by default. -#.vscode/ +.vscode/ # Flutter/Dart/Pub related **/doc/api/ @@ -29,6 +29,7 @@ .pub-cache/ .pub/ build/ +coverage/ # Android related **/android/**/gradle-wrapper.jar From e0abcde0a4ddc4ae8d07df426c67c11f1d4a918d Mon Sep 17 00:00:00 2001 From: Yurii Prykhodko Date: Thu, 26 Aug 2021 14:29:26 +0300 Subject: [PATCH 17/21] Remove unused class, fix typo --- test/hydrated_test.dart | 2 +- test/shared_preferences_persistence_test.dart | 25 ------------------- 2 files changed, 1 insertion(+), 26 deletions(-) diff --git a/test/hydrated_test.dart b/test/hydrated_test.dart index cf94609..11874b6 100644 --- a/test/hydrated_test.dart +++ b/test/hydrated_test.dart @@ -189,7 +189,7 @@ void main() { }); test( - 'AAAA given persistence interface put throws a PersistenceError, ' + 'given persistence interface put throws a PersistenceError, ' 'it emits the error through the stream', () async { const testValue = 42; mockKeyValueStore.putOverride = diff --git a/test/shared_preferences_persistence_test.dart b/test/shared_preferences_persistence_test.dart index 0dba899..44496f2 100644 --- a/test/shared_preferences_persistence_test.dart +++ b/test/shared_preferences_persistence_test.dart @@ -86,31 +86,6 @@ void main() { }); } -/// An example of a class that serializes to and from a string -class SerializedClass extends Equatable { - final bool value; - final int count; - - SerializedClass(this.value, this.count); - - factory SerializedClass.fromJSON(String s) { - final map = jsonDecode(s); - - return SerializedClass( - map['value'], - map['count'], - ); - } - - String toJSON() => jsonEncode({ - 'value': this.value, - 'count': this.count, - }); - - @override - List get props => [value, count]; -} - void _setMockPersistedValue(String key, dynamic value) { SharedPreferences.setMockInitialValues({ "flutter.$key": value, From cb45bca91c49fd121553e03a540b31a5bf594ca5 Mon Sep 17 00:00:00 2001 From: Yurii Prykhodko Date: Thu, 26 Aug 2021 14:32:35 +0300 Subject: [PATCH 18/21] Remove unused imports --- test/shared_preferences_persistence_test.dart | 2 -- 1 file changed, 2 deletions(-) diff --git a/test/shared_preferences_persistence_test.dart b/test/shared_preferences_persistence_test.dart index 44496f2..f80df51 100644 --- a/test/shared_preferences_persistence_test.dart +++ b/test/shared_preferences_persistence_test.dart @@ -1,7 +1,5 @@ import 'dart:async'; -import 'dart:convert'; -import 'package:equatable/equatable.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:hydrated/hydrated.dart'; import 'package:shared_preferences/shared_preferences.dart'; From b7695d5f1e82321d85c80533aa209578ad8c5341 Mon Sep 17 00:00:00 2001 From: Yurii Prykhodko Date: Thu, 26 Aug 2021 14:51:43 +0300 Subject: [PATCH 19/21] Rename SharedPreferencesPersistence -> SharedPreferencesStore --- lib/hydrated.dart | 2 +- lib/src/hydrated.dart | 6 +++--- ...ersistence.dart => shared_preferences_store.dart} | 4 ++-- ..._test.dart => shared_preferences_store_test.dart} | 12 ++++++------ 4 files changed, 12 insertions(+), 12 deletions(-) rename lib/src/persistence/{shared_preferences_persistence.dart => shared_preferences_store.dart} (95%) rename test/{shared_preferences_persistence_test.dart => shared_preferences_store_test.dart} (89%) diff --git a/lib/hydrated.dart b/lib/hydrated.dart index 7840d86..912da67 100644 --- a/lib/hydrated.dart +++ b/lib/hydrated.dart @@ -3,4 +3,4 @@ library hydrated; export 'src/hydrated.dart'; export 'src/persistence/key_value_store.dart'; export 'src/persistence/persistence_error.dart'; -export 'src/persistence/shared_preferences_persistence.dart'; +export 'src/persistence/shared_preferences_store.dart'; diff --git a/lib/src/hydrated.dart b/lib/src/hydrated.dart index 45a789c..41374f9 100644 --- a/lib/src/hydrated.dart +++ b/lib/src/hydrated.dart @@ -5,7 +5,7 @@ import 'package:rxdart/rxdart.dart'; import 'persistence/key_value_store.dart'; import 'persistence/persistence_error.dart'; -import 'persistence/shared_preferences_persistence.dart'; +import 'persistence/shared_preferences_store.dart'; /// A callback for encoding an instance of a data class into a String. typedef PersistCallback = String? Function(T); @@ -18,7 +18,7 @@ typedef HydrateCallback = T Function(String); /// Mimics the behavior of a [BehaviorSubject]. /// /// The set of supported classes depends on the [KeyValueStore] implementation. -/// For a list of types supported by default see [SharedPreferencesPersistence]. +/// For a list of types supported by default see [SharedPreferencesStore]. /// /// Example: /// @@ -81,7 +81,7 @@ class HydratedSubject extends Subject implements ValueStream { VoidCallback? onListen, VoidCallback? onCancel, bool sync = false, - KeyValueStore persistence = const SharedPreferencesPersistence(), + KeyValueStore persistence = const SharedPreferencesStore(), }) { assert( (hydrate == null && persist == null) || diff --git a/lib/src/persistence/shared_preferences_persistence.dart b/lib/src/persistence/shared_preferences_store.dart similarity index 95% rename from lib/src/persistence/shared_preferences_persistence.dart rename to lib/src/persistence/shared_preferences_store.dart index f62ea06..a201525 100644 --- a/lib/src/persistence/shared_preferences_persistence.dart +++ b/lib/src/persistence/shared_preferences_store.dart @@ -12,10 +12,10 @@ import 'persistence_error.dart'; /// - `bool` /// - `String` /// - `List`. -class SharedPreferencesPersistence implements KeyValueStore { +class SharedPreferencesStore implements KeyValueStore { static final _areTypesEqual = TypeUtils.areTypesEqual; - const SharedPreferencesPersistence(); + const SharedPreferencesStore(); @override Future get(String key) async { diff --git a/test/shared_preferences_persistence_test.dart b/test/shared_preferences_store_test.dart similarity index 89% rename from test/shared_preferences_persistence_test.dart rename to test/shared_preferences_store_test.dart index f80df51..d3fdf25 100644 --- a/test/shared_preferences_persistence_test.dart +++ b/test/shared_preferences_store_test.dart @@ -18,14 +18,14 @@ void main() { expect(value, isTrue); }); - group('SharedPreferencesPersistence', () { + group('SharedPreferencesStore', () { group('handles unsupported types', () { test( 'when saving a value with an unsupported type, it throws an AssertionError', () { final unsupportedTypeValue = Exception('test unsupported value'); expect( - () => SharedPreferencesPersistence().put('key', unsupportedTypeValue), + () => SharedPreferencesStore().put('key', unsupportedTypeValue), throwsA(isA()), ); }); @@ -34,7 +34,7 @@ void main() { 'when getting a value with an unspecified type (dynamic), it throws an AssertionError', () { expect( - () => SharedPreferencesPersistence().get('key'), + () => SharedPreferencesStore().get('key'), throwsA(isA()), ); }); @@ -43,7 +43,7 @@ void main() { 'when getting a value with an unsupported type, it throws an AssertionError', () { expect( - () => SharedPreferencesPersistence().get('key'), + () => SharedPreferencesStore().get('key'), throwsA(isA()), ); }); @@ -54,7 +54,7 @@ void main() { final unsupportedTypeValue = Exception('test unsupported value'); _setMockPersistedValue('key', unsupportedTypeValue); expect( - () => SharedPreferencesPersistence().get('key'), + () => SharedPreferencesStore().get('key'), throwsA(isA()), ); }); @@ -96,7 +96,7 @@ Future _testPersistence( T first, T second, ) async { - final persistence = SharedPreferencesPersistence(); + final persistence = SharedPreferencesStore(); /// null before setting anything expect(await persistence.get(key), isNull); From 8459c214c6368ea0b8b27a136a8bf2578f7b7aa3 Mon Sep 17 00:00:00 2001 From: Yurii Prykhodko Date: Thu, 26 Aug 2021 15:07:32 +0300 Subject: [PATCH 20/21] Rename PersistenceError -> StoreError --- lib/hydrated.dart | 6 +- lib/src/hydrated.dart | 14 +- .../key_value_store.dart | 6 +- .../shared_preferences_store.dart | 4 +- .../store_error.dart} | 4 +- test/hydrated_behavior_test.dart | 186 +++++++++--------- test/hydrated_test.dart | 32 +-- test/shared_preferences_store_test.dart | 2 +- 8 files changed, 127 insertions(+), 127 deletions(-) rename lib/src/{persistence => key_value_store}/key_value_store.dart (59%) rename lib/src/{persistence => key_value_store}/shared_preferences_store.dart (97%) rename lib/src/{persistence/persistence_error.dart => key_value_store/store_error.dart} (74%) diff --git a/lib/hydrated.dart b/lib/hydrated.dart index 912da67..c5047b2 100644 --- a/lib/hydrated.dart +++ b/lib/hydrated.dart @@ -1,6 +1,6 @@ library hydrated; export 'src/hydrated.dart'; -export 'src/persistence/key_value_store.dart'; -export 'src/persistence/persistence_error.dart'; -export 'src/persistence/shared_preferences_store.dart'; +export 'src/key_value_store/key_value_store.dart'; +export 'src/key_value_store/store_error.dart'; +export 'src/key_value_store/shared_preferences_store.dart'; diff --git a/lib/src/hydrated.dart b/lib/src/hydrated.dart index 41374f9..1be1a30 100644 --- a/lib/src/hydrated.dart +++ b/lib/src/hydrated.dart @@ -3,9 +3,9 @@ import 'dart:async'; import 'package:flutter/foundation.dart'; import 'package:rxdart/rxdart.dart'; -import 'persistence/key_value_store.dart'; -import 'persistence/persistence_error.dart'; -import 'persistence/shared_preferences_store.dart'; +import 'key_value_store/key_value_store.dart'; +import 'key_value_store/store_error.dart'; +import 'key_value_store/shared_preferences_store.dart'; /// A callback for encoding an instance of a data class into a String. typedef PersistCallback = String? Function(T); @@ -81,7 +81,7 @@ class HydratedSubject extends Subject implements ValueStream { VoidCallback? onListen, VoidCallback? onCancel, bool sync = false, - KeyValueStore persistence = const SharedPreferencesStore(), + KeyValueStore keyValueStore = const SharedPreferencesStore(), }) { assert( (hydrate == null && persist == null) || @@ -108,7 +108,7 @@ class HydratedSubject extends Subject implements ValueStream { persist, onHydrate, subject, - persistence, + keyValueStore, ); } @@ -168,7 +168,7 @@ class HydratedSubject extends Subject implements ValueStream { } _onHydrate?.call(); - } on PersistenceError catch (e, s) { + } on StoreError catch (e, s) { addError(e, s); } } @@ -184,7 +184,7 @@ class HydratedSubject extends Subject implements ValueStream { persistedVal = val; await _persistence.put(_key, persistedVal); } - } on PersistenceError catch (e, s) { + } on StoreError catch (e, s) { addError(e, s); } } diff --git a/lib/src/persistence/key_value_store.dart b/lib/src/key_value_store/key_value_store.dart similarity index 59% rename from lib/src/persistence/key_value_store.dart rename to lib/src/key_value_store/key_value_store.dart index 0f38ca2..61f57bc 100644 --- a/lib/src/persistence/key_value_store.dart +++ b/lib/src/key_value_store/key_value_store.dart @@ -1,4 +1,4 @@ -import 'persistence_error.dart'; +import 'store_error.dart'; /// A generic key-value persistence interface. abstract class KeyValueStore { @@ -6,11 +6,11 @@ abstract class KeyValueStore { /// /// Passing a `null` should clear the value for this [key]. /// - /// Throw a [PersistenceError] if encountering a problem while persisting a value. + /// Throw a [StoreError] if encountering a problem while persisting a value. Future put(String key, T? value); /// Retrieve a value from persistence. /// - /// Throw a [PersistenceError] if encountering a problem while restoring a value from the storage. + /// Throw a [StoreError] if encountering a problem while restoring a value from the storage. Future get(String key); } diff --git a/lib/src/persistence/shared_preferences_store.dart b/lib/src/key_value_store/shared_preferences_store.dart similarity index 97% rename from lib/src/persistence/shared_preferences_store.dart rename to lib/src/key_value_store/shared_preferences_store.dart index a201525..6007631 100644 --- a/lib/src/persistence/shared_preferences_store.dart +++ b/lib/src/key_value_store/shared_preferences_store.dart @@ -2,7 +2,7 @@ import 'package:shared_preferences/shared_preferences.dart'; import '../utils/type_utils.dart'; import 'key_value_store.dart'; -import 'persistence_error.dart'; +import 'store_error.dart'; /// An adapter for [SharedPreferences] persistence. /// @@ -37,7 +37,7 @@ class SharedPreferencesStore implements KeyValueStore { _areTypesEqual?>()) val = prefs.getStringList(key) as T?; } catch (e) { - throw PersistenceError( + throw StoreError( 'Error retrieving value from SharedPreferences: $e', ); } diff --git a/lib/src/persistence/persistence_error.dart b/lib/src/key_value_store/store_error.dart similarity index 74% rename from lib/src/persistence/persistence_error.dart rename to lib/src/key_value_store/store_error.dart index 2343a9b..4aad0e4 100644 --- a/lib/src/persistence/persistence_error.dart +++ b/lib/src/key_value_store/store_error.dart @@ -1,8 +1,8 @@ /// An error encountered when persisting a value, or restoring it from persistence. -class PersistenceError extends Error { +class StoreError extends Error { /// A description of an error. final String message; /// A persistence error with a [message] describing its details. - PersistenceError(this.message); + StoreError(this.message); } diff --git a/test/hydrated_behavior_test.dart b/test/hydrated_behavior_test.dart index 1599de2..9d7b99f 100644 --- a/test/hydrated_behavior_test.dart +++ b/test/hydrated_behavior_test.dart @@ -25,9 +25,9 @@ void main() { group('HydratedSubject', () { test('emits the most recently emitted item to every subscriber', () async { final unseeded = - HydratedSubject('key', persistence: mockKeyValueStore), + HydratedSubject('key', keyValueStore: mockKeyValueStore), seeded = HydratedSubject('key', - seedValue: 0, persistence: mockKeyValueStore); + seedValue: 0, keyValueStore: mockKeyValueStore); unseeded.add(1); unseeded.add(2); @@ -49,9 +49,9 @@ void main() { test('emits the most recently emitted null item to every subscriber', () async { final unseeded = - HydratedSubject('key', persistence: mockKeyValueStore), + HydratedSubject('key', keyValueStore: mockKeyValueStore), seeded = HydratedSubject('key', - seedValue: 0, persistence: mockKeyValueStore); + seedValue: 0, keyValueStore: mockKeyValueStore); unseeded.add(1); unseeded.add(2); @@ -74,9 +74,9 @@ void main() { 'emits the most recently emitted item to every subscriber that subscribe to the subject directly', () async { final unseeded = - HydratedSubject('key', persistence: mockKeyValueStore), + HydratedSubject('key', keyValueStore: mockKeyValueStore), seeded = HydratedSubject('key', - seedValue: 0, persistence: mockKeyValueStore); + seedValue: 0, keyValueStore: mockKeyValueStore); unseeded.add(1); unseeded.add(2); @@ -97,9 +97,9 @@ void main() { test('emits errors to every subscriber', () async { final unseeded = - HydratedSubject('key', persistence: mockKeyValueStore), + HydratedSubject('key', keyValueStore: mockKeyValueStore), seeded = HydratedSubject('key', - seedValue: 0, persistence: mockKeyValueStore); + seedValue: 0, keyValueStore: mockKeyValueStore); unseeded.add(1); unseeded.add(2); @@ -122,9 +122,9 @@ void main() { test('emits event after error to every subscriber', () async { final unseeded = - HydratedSubject('key', persistence: mockKeyValueStore), + HydratedSubject('key', keyValueStore: mockKeyValueStore), seeded = HydratedSubject('key', - seedValue: 0, persistence: mockKeyValueStore); + seedValue: 0, keyValueStore: mockKeyValueStore); unseeded.add(1); unseeded.add(2); @@ -147,9 +147,9 @@ void main() { test('emits errors to every subscriber', () async { final unseeded = - HydratedSubject('key', persistence: mockKeyValueStore), + HydratedSubject('key', keyValueStore: mockKeyValueStore), seeded = HydratedSubject('key', - seedValue: 0, persistence: mockKeyValueStore); + seedValue: 0, keyValueStore: mockKeyValueStore); final exception = Exception('oh noes!'); unseeded.add(1); @@ -189,9 +189,9 @@ void main() { test('can synchronously get the latest value', () { final unseeded = - HydratedSubject('key', persistence: mockKeyValueStore), + HydratedSubject('key', keyValueStore: mockKeyValueStore), seeded = HydratedSubject('key', - seedValue: 0, persistence: mockKeyValueStore); + seedValue: 0, keyValueStore: mockKeyValueStore); unseeded.add(1); unseeded.add(2); @@ -212,9 +212,9 @@ void main() { test('can synchronously get the latest null value', () async { final unseeded = - HydratedSubject('key', persistence: mockKeyValueStore), + HydratedSubject('key', keyValueStore: mockKeyValueStore), seeded = HydratedSubject('key', - seedValue: 0, persistence: mockKeyValueStore); + seedValue: 0, keyValueStore: mockKeyValueStore); unseeded.add(1); unseeded.add(2); @@ -235,7 +235,7 @@ void main() { test('emits the seed item if no new items have been emitted', () async { final subject = HydratedSubject('key', - seedValue: 1, persistence: mockKeyValueStore); + seedValue: 1, keyValueStore: mockKeyValueStore); await expectLater(subject.stream, emits(1)); await expectLater(subject.stream, emits(1)); @@ -244,7 +244,7 @@ void main() { test('can synchronously get the initial value', () { final subject = HydratedSubject('key', - seedValue: 1, persistence: mockKeyValueStore); + seedValue: 1, keyValueStore: mockKeyValueStore); expect(subject.value, 1); expect(subject.valueOrNull, 1); @@ -253,7 +253,7 @@ void main() { test('cannot synchronously get the initial null value', () { final subject = HydratedSubject('key', - seedValue: null, persistence: mockKeyValueStore); + seedValue: null, keyValueStore: mockKeyValueStore); expect(subject.hasValue, false); expect(subject.valueOrNull, null); @@ -261,7 +261,7 @@ void main() { test('initial value is null when no value has been emitted', () { final subject = - HydratedSubject('key', persistence: mockKeyValueStore); + HydratedSubject('key', keyValueStore: mockKeyValueStore); expect(() => subject.value, throwsValueStreamError); expect(subject.valueOrNull, null); @@ -270,9 +270,9 @@ void main() { test('emits done event to listeners when the subject is closed', () async { final unseeded = - HydratedSubject('key', persistence: mockKeyValueStore), + HydratedSubject('key', keyValueStore: mockKeyValueStore), seeded = HydratedSubject('key', - seedValue: 0, persistence: mockKeyValueStore); + seedValue: 0, keyValueStore: mockKeyValueStore); await expectLater(unseeded.isClosed, isFalse); await expectLater(seeded.isClosed, isFalse); @@ -292,9 +292,9 @@ void main() { test('emits error events to subscribers', () async { final unseeded = - HydratedSubject('key', persistence: mockKeyValueStore), + HydratedSubject('key', keyValueStore: mockKeyValueStore), seeded = HydratedSubject('key', - seedValue: 0, persistence: mockKeyValueStore); + seedValue: 0, keyValueStore: mockKeyValueStore); scheduleMicrotask(() => unseeded.addError(Exception())); scheduleMicrotask(() => seeded.addError(Exception())); @@ -305,9 +305,9 @@ void main() { test('replays the previously emitted items from addStream', () async { final unseeded = - HydratedSubject('key', persistence: mockKeyValueStore), + HydratedSubject('key', keyValueStore: mockKeyValueStore), seeded = HydratedSubject('key', - seedValue: 0, persistence: mockKeyValueStore); + seedValue: 0, keyValueStore: mockKeyValueStore); await unseeded.addStream(Stream.fromIterable(const [1, 2, 3])); await seeded.addStream(Stream.fromIterable(const [1, 2, 3])); @@ -323,9 +323,9 @@ void main() { test('replays the previously emitted errors from addStream', () async { final unseeded = - HydratedSubject('key', persistence: mockKeyValueStore), + HydratedSubject('key', keyValueStore: mockKeyValueStore), seeded = HydratedSubject('key', - seedValue: 0, persistence: mockKeyValueStore); + seedValue: 0, keyValueStore: mockKeyValueStore); await unseeded.addStream(Stream.error('error'), cancelOnError: false); @@ -337,7 +337,7 @@ void main() { test('allows items to be added once addStream is complete', () async { final subject = - HydratedSubject('key', persistence: mockKeyValueStore); + HydratedSubject('key', keyValueStore: mockKeyValueStore); await subject.addStream(Stream.fromIterable(const [1, 2])); subject.add(3); @@ -348,7 +348,7 @@ void main() { test('allows items to be added once addStream completes with an error', () async { final subject = - HydratedSubject('key', persistence: mockKeyValueStore); + HydratedSubject('key', keyValueStore: mockKeyValueStore); subject .addStream(Stream.error(Exception()), cancelOnError: true) @@ -361,7 +361,7 @@ void main() { test('does not allow events to be added when addStream is active', () async { final subject = - HydratedSubject('key', persistence: mockKeyValueStore); + HydratedSubject('key', keyValueStore: mockKeyValueStore); // Purposely don't wait for the future to complete, then try to add items @@ -373,7 +373,7 @@ void main() { test('does not allow errors to be added when addStream is active', () async { final subject = - HydratedSubject('key', persistence: mockKeyValueStore); + HydratedSubject('key', keyValueStore: mockKeyValueStore); // Purposely don't wait for the future to complete, then try to add items @@ -385,7 +385,7 @@ void main() { test('does not allow subject to be closed when addStream is active', () async { final subject = - HydratedSubject('key', persistence: mockKeyValueStore); + HydratedSubject('key', keyValueStore: mockKeyValueStore); // Purposely don't wait for the future to complete, then try to add items @@ -398,7 +398,7 @@ void main() { 'does not allow addStream to add items when previous addStream is active', () async { final subject = - HydratedSubject('key', persistence: mockKeyValueStore); + HydratedSubject('key', keyValueStore: mockKeyValueStore); // Purposely don't wait for the future to complete, then try to add items @@ -412,7 +412,7 @@ void main() { final testOnListen = () {}; final subject = HydratedSubject('key', - onListen: testOnListen, persistence: mockKeyValueStore); + onListen: testOnListen, keyValueStore: mockKeyValueStore); await expectLater(subject.onListen, testOnListen); }); @@ -421,7 +421,7 @@ void main() { final testOnListen = () {}; final subject = - HydratedSubject('key', persistence: mockKeyValueStore); + HydratedSubject('key', keyValueStore: mockKeyValueStore); await expectLater(subject.onListen, isNull); @@ -434,7 +434,7 @@ void main() { final onCancel = () => Future.value(null); final subject = HydratedSubject('key', - onCancel: onCancel, persistence: mockKeyValueStore); + onCancel: onCancel, keyValueStore: mockKeyValueStore); await expectLater(subject.onCancel, onCancel); }); @@ -443,7 +443,7 @@ void main() { final testOnCancel = () {}; final subject = - HydratedSubject('key', persistence: mockKeyValueStore); + HydratedSubject('key', keyValueStore: mockKeyValueStore); await expectLater(subject.onCancel, isNull); @@ -454,7 +454,7 @@ void main() { test('reports if a listener is present', () async { final subject = - HydratedSubject('key', persistence: mockKeyValueStore); + HydratedSubject('key', keyValueStore: mockKeyValueStore); await expectLater(subject.hasListener, isFalse); @@ -465,7 +465,7 @@ void main() { test('onPause unsupported', () { final subject = - HydratedSubject('key', persistence: mockKeyValueStore); + HydratedSubject('key', keyValueStore: mockKeyValueStore); expect(subject.isPaused, isFalse); expect(() => subject.onPause, throwsUnsupportedError); @@ -474,7 +474,7 @@ void main() { test('onResume unsupported', () { final subject = - HydratedSubject('key', persistence: mockKeyValueStore); + HydratedSubject('key', keyValueStore: mockKeyValueStore); expect(() => subject.onResume, throwsUnsupportedError); expect(() => subject.onResume = () {}, throwsUnsupportedError); @@ -482,14 +482,14 @@ void main() { test('returns controller sink', () async { final subject = - HydratedSubject('key', persistence: mockKeyValueStore); + HydratedSubject('key', keyValueStore: mockKeyValueStore); await expectLater(subject.sink, TypeMatcher>()); }); test('correctly closes done Future', () async { final subject = - HydratedSubject('key', persistence: mockKeyValueStore); + HydratedSubject('key', keyValueStore: mockKeyValueStore); scheduleMicrotask(() => subject.close()); @@ -498,7 +498,7 @@ void main() { test('can be listened to multiple times', () async { final subject = - HydratedSubject('key', seedValue: 1, persistence: mockKeyValueStore); + HydratedSubject('key', seedValue: 1, keyValueStore: mockKeyValueStore); final stream = subject.stream; await expectLater(stream, emits(1)); @@ -507,7 +507,7 @@ void main() { test('always returns the same stream', () async { final subject = - HydratedSubject('key', persistence: mockKeyValueStore); + HydratedSubject('key', keyValueStore: mockKeyValueStore); await expectLater(subject.stream, equals(subject.stream)); }); @@ -515,7 +515,7 @@ void main() { test('adding to sink has same behavior as adding to Subject itself', () async { final subject = - HydratedSubject('key', persistence: mockKeyValueStore); + HydratedSubject('key', keyValueStore: mockKeyValueStore); subject.sink.add(1); @@ -531,7 +531,7 @@ void main() { test('setter `value=` has same behavior as adding to Subject', () async { final subject = - HydratedSubject('key', seedValue: 0, persistence: mockKeyValueStore); + HydratedSubject('key', seedValue: 0, keyValueStore: mockKeyValueStore); subject.value = 1; @@ -550,7 +550,7 @@ void main() { test('is always treated as a broadcast Stream', () async { final subject = - HydratedSubject('key', persistence: mockKeyValueStore); + HydratedSubject('key', keyValueStore: mockKeyValueStore); final stream = subject.asyncMap((event) => Future.value(event)); expect(subject.isBroadcast, isTrue); @@ -559,28 +559,28 @@ void main() { test('hasValue returns false for an empty subject', () { final subject = - HydratedSubject('key', persistence: mockKeyValueStore); + HydratedSubject('key', keyValueStore: mockKeyValueStore); expect(subject.hasValue, isFalse); }); test('hasValue returns true for a seeded subject with non-null seed', () { final subject = HydratedSubject('key', - seedValue: 1, persistence: mockKeyValueStore); + seedValue: 1, keyValueStore: mockKeyValueStore); expect(subject.hasValue, isTrue); }); test('hasValue returns false for a seeded subject with null seed', () { final subject = HydratedSubject('key', - seedValue: null, persistence: mockKeyValueStore); + seedValue: null, keyValueStore: mockKeyValueStore); expect(subject.hasValue, isFalse); }); test('hasValue returns true for an unseeded subject after an emission', () { final subject = - HydratedSubject('key', persistence: mockKeyValueStore); + HydratedSubject('key', keyValueStore: mockKeyValueStore); subject.add(1); @@ -589,21 +589,21 @@ void main() { test('hasError returns false for an empty subject', () { final subject = - HydratedSubject('key', persistence: mockKeyValueStore); + HydratedSubject('key', keyValueStore: mockKeyValueStore); expect(subject.hasError, isFalse); }); test('hasError returns false for a seeded subject with non-null seed', () { final subject = HydratedSubject('key', - seedValue: 1, persistence: mockKeyValueStore); + seedValue: 1, keyValueStore: mockKeyValueStore); expect(subject.hasError, isFalse); }); test('hasError returns false for a seeded subject with null seed', () { final subject = HydratedSubject('key', - seedValue: null, persistence: mockKeyValueStore); + seedValue: null, keyValueStore: mockKeyValueStore); expect(subject.hasError, isFalse); }); @@ -611,7 +611,7 @@ void main() { test('hasError returns false for an unseeded subject after an emission', () { final subject = - HydratedSubject('key', persistence: mockKeyValueStore); + HydratedSubject('key', keyValueStore: mockKeyValueStore); subject.add(1); @@ -620,7 +620,7 @@ void main() { test('hasError returns true for an unseeded subject after addError', () { final subject = - HydratedSubject('key', persistence: mockKeyValueStore); + HydratedSubject('key', keyValueStore: mockKeyValueStore); subject.add(1); subject.addError('error'); @@ -630,7 +630,7 @@ void main() { test('hasError returns true for a seeded subject after addError', () { final subject = HydratedSubject('key', - seedValue: 1, persistence: mockKeyValueStore); + seedValue: 1, keyValueStore: mockKeyValueStore); subject.addError('error'); @@ -639,7 +639,7 @@ void main() { test('error returns null for an empty subject', () { final subject = - HydratedSubject('key', persistence: mockKeyValueStore); + HydratedSubject('key', keyValueStore: mockKeyValueStore); expect(subject.hasError, isFalse); expect(subject.errorOrNull, isNull); @@ -648,7 +648,7 @@ void main() { test('error returns null for a seeded subject with non-null seed', () { final subject = HydratedSubject('key', - seedValue: 1, persistence: mockKeyValueStore); + seedValue: 1, keyValueStore: mockKeyValueStore); expect(subject.hasError, isFalse); expect(subject.errorOrNull, isNull); @@ -657,7 +657,7 @@ void main() { test('error returns null for a seeded subject with null seed', () { final subject = HydratedSubject('key', - seedValue: null, persistence: mockKeyValueStore); + seedValue: null, keyValueStore: mockKeyValueStore); expect(subject.hasError, isFalse); expect(subject.errorOrNull, isNull); @@ -666,9 +666,9 @@ void main() { test('can synchronously get the latest error', () async { final unseeded = - HydratedSubject('key', persistence: mockKeyValueStore), + HydratedSubject('key', keyValueStore: mockKeyValueStore), seeded = HydratedSubject('key', - seedValue: 0, persistence: mockKeyValueStore); + seedValue: 0, keyValueStore: mockKeyValueStore); unseeded.add(1); unseeded.add(2); @@ -697,9 +697,9 @@ void main() { test('emits event after error to every subscriber', () async { final unseeded = - HydratedSubject('key', persistence: mockKeyValueStore), + HydratedSubject('key', keyValueStore: mockKeyValueStore), seeded = HydratedSubject('key', - seedValue: 0, persistence: mockKeyValueStore); + seedValue: 0, keyValueStore: mockKeyValueStore); unseeded.add(1); unseeded.add(2); @@ -726,7 +726,7 @@ void main() { group('override built-in', () { test('where', () { { - var hydratedSubject = HydratedSubject('key', seedValue: 1, persistence: mockKeyValueStore); + var hydratedSubject = HydratedSubject('key', seedValue: 1, keyValueStore: mockKeyValueStore); var stream = hydratedSubject.where((event) => event.isOdd); expect(stream, emitsInOrder([1, 3])); @@ -737,7 +737,7 @@ void main() { { var hydratedSubject = - HydratedSubject('key', persistence: mockKeyValueStore); + HydratedSubject('key', keyValueStore: mockKeyValueStore); var stream = hydratedSubject.where((event) => event?.isOdd ?? false); expect(stream, emitsInOrder([1, 3])); @@ -750,7 +750,7 @@ void main() { test('map', () { { - var hydratedSubject = HydratedSubject('key', seedValue: 1, persistence: mockKeyValueStore); + var hydratedSubject = HydratedSubject('key', seedValue: 1, keyValueStore: mockKeyValueStore); var mapped = hydratedSubject.map((event) => event + 1); expect(mapped, emitsInOrder([2, 3])); @@ -760,7 +760,7 @@ void main() { { var hydratedSubject = - HydratedSubject('key', persistence: mockKeyValueStore); + HydratedSubject('key', keyValueStore: mockKeyValueStore); var mapped = hydratedSubject.map((event) => (event ?? 0) + 1); expect(mapped, emitsInOrder([2, 3])); @@ -772,7 +772,7 @@ void main() { test('asyncMap', () { { - var hydratedSubject = HydratedSubject('key', seedValue: 1, persistence: mockKeyValueStore); + var hydratedSubject = HydratedSubject('key', seedValue: 1, keyValueStore: mockKeyValueStore); var mapped = hydratedSubject.asyncMap((event) => Future.value(event + 1)); @@ -783,7 +783,7 @@ void main() { { var hydratedSubject = - HydratedSubject('key', persistence: mockKeyValueStore); + HydratedSubject('key', keyValueStore: mockKeyValueStore); var mapped = hydratedSubject .asyncMap((event) => Future.value((event ?? 0) + 1)); @@ -796,7 +796,7 @@ void main() { test('asyncExpand', () { { - var hydratedSubject = HydratedSubject('key', seedValue: 1, persistence: mockKeyValueStore); + var hydratedSubject = HydratedSubject('key', seedValue: 1, keyValueStore: mockKeyValueStore); var stream = hydratedSubject.asyncExpand((event) => Stream.value(event + 1)); @@ -807,7 +807,7 @@ void main() { { var hydratedSubject = - HydratedSubject('key', persistence: mockKeyValueStore); + HydratedSubject('key', keyValueStore: mockKeyValueStore); var stream = hydratedSubject.asyncExpand((event) => Stream.value(event! + 1)); @@ -820,7 +820,7 @@ void main() { test('handleError', () { { - var hydratedSubject = HydratedSubject('key', seedValue: 1, persistence: mockKeyValueStore); + var hydratedSubject = HydratedSubject('key', seedValue: 1, keyValueStore: mockKeyValueStore); var stream = hydratedSubject.handleError( expectAsync1( @@ -840,7 +840,7 @@ void main() { { var hydratedSubject = - HydratedSubject('key', persistence: mockKeyValueStore); + HydratedSubject('key', keyValueStore: mockKeyValueStore); var stream = hydratedSubject.handleError( expectAsync1( @@ -862,7 +862,7 @@ void main() { test('expand', () { { - var hydratedSubject = HydratedSubject('key', seedValue: 1, persistence: mockKeyValueStore); + var hydratedSubject = HydratedSubject('key', seedValue: 1, keyValueStore: mockKeyValueStore); var stream = hydratedSubject.expand((event) => [event + 1]); expect(stream, emitsInOrder([2, 3])); @@ -872,7 +872,7 @@ void main() { { var hydratedSubject = - HydratedSubject('key', persistence: mockKeyValueStore); + HydratedSubject('key', keyValueStore: mockKeyValueStore); var stream = hydratedSubject.expand((event) => [event! + 1]); expect(stream, emitsInOrder([2, 3])); @@ -884,7 +884,7 @@ void main() { test('transform', () { { - var hydratedSubject = HydratedSubject('key', seedValue: 1, persistence: mockKeyValueStore); + var hydratedSubject = HydratedSubject('key', seedValue: 1, keyValueStore: mockKeyValueStore); var stream = hydratedSubject.transform( IntervalStreamTransformer(const Duration(milliseconds: 100))); @@ -895,7 +895,7 @@ void main() { { var hydratedSubject = - HydratedSubject('key', persistence: mockKeyValueStore); + HydratedSubject('key', keyValueStore: mockKeyValueStore); var stream = hydratedSubject.transform( IntervalStreamTransformer(const Duration(milliseconds: 100))); @@ -909,7 +909,7 @@ void main() { test('cast', () { { var hydratedSubject = HydratedSubject('key', - seedValue: 1, persistence: mockKeyValueStore); + seedValue: 1, keyValueStore: mockKeyValueStore); var stream = hydratedSubject.cast(); expect(stream, emitsInOrder([1, 2])); @@ -919,7 +919,7 @@ void main() { { var hydratedSubject = - HydratedSubject('key', persistence: mockKeyValueStore); + HydratedSubject('key', keyValueStore: mockKeyValueStore); var stream = hydratedSubject.cast(); expect(stream, emitsInOrder([1, 2])); @@ -931,7 +931,7 @@ void main() { test('take', () { { - var hydratedSubject = HydratedSubject('key', seedValue: 1, persistence: mockKeyValueStore); + var hydratedSubject = HydratedSubject('key', seedValue: 1, keyValueStore: mockKeyValueStore); var stream = hydratedSubject.take(2); expect(stream, emitsInOrder([1, 2])); @@ -942,7 +942,7 @@ void main() { { var hydratedSubject = - HydratedSubject('key', persistence: mockKeyValueStore); + HydratedSubject('key', keyValueStore: mockKeyValueStore); var stream = hydratedSubject.take(2); expect(stream, emitsInOrder([1, 2])); @@ -955,7 +955,7 @@ void main() { test('takeWhile', () { { - var hydratedSubject = HydratedSubject('key', seedValue: 1, persistence: mockKeyValueStore); + var hydratedSubject = HydratedSubject('key', seedValue: 1, keyValueStore: mockKeyValueStore); var stream = hydratedSubject.takeWhile((element) => element <= 2); expect(stream, emitsInOrder([1, 2])); @@ -966,7 +966,7 @@ void main() { { var hydratedSubject = - HydratedSubject('key', persistence: mockKeyValueStore); + HydratedSubject('key', keyValueStore: mockKeyValueStore); var stream = hydratedSubject.takeWhile((element) => element! <= 2); expect(stream, emitsInOrder([1, 2])); @@ -979,7 +979,7 @@ void main() { test('skip', () { { - var hydratedSubject = HydratedSubject('key', seedValue: 1, persistence: mockKeyValueStore); + var hydratedSubject = HydratedSubject('key', seedValue: 1, keyValueStore: mockKeyValueStore); var stream = hydratedSubject.skip(2); expect(stream, emitsInOrder([3, 4])); @@ -991,7 +991,7 @@ void main() { { var hydratedSubject = - HydratedSubject('key', persistence: mockKeyValueStore); + HydratedSubject('key', keyValueStore: mockKeyValueStore); var stream = hydratedSubject.skip(2); expect(stream, emitsInOrder([3, 4])); @@ -1005,7 +1005,7 @@ void main() { test('skipWhile', () { { - var hydratedSubject = HydratedSubject('key', seedValue: 1, persistence: mockKeyValueStore); + var hydratedSubject = HydratedSubject('key', seedValue: 1, keyValueStore: mockKeyValueStore); var stream = hydratedSubject.skipWhile((element) => element < 3); expect(stream, emitsInOrder([3, 4])); @@ -1017,7 +1017,7 @@ void main() { { var hydratedSubject = - HydratedSubject('key', persistence: mockKeyValueStore); + HydratedSubject('key', keyValueStore: mockKeyValueStore); var stream = hydratedSubject.skipWhile((element) => element! < 3); expect(stream, emitsInOrder([3, 4])); @@ -1031,7 +1031,7 @@ void main() { test('distinct', () { { - var hydratedSubject = HydratedSubject('key', seedValue: 1, persistence: mockKeyValueStore); + var hydratedSubject = HydratedSubject('key', seedValue: 1, keyValueStore: mockKeyValueStore); var stream = hydratedSubject.distinct(); expect(stream, emitsInOrder([1, 2])); @@ -1043,7 +1043,7 @@ void main() { { var hydratedSubject = - HydratedSubject('key', persistence: mockKeyValueStore); + HydratedSubject('key', keyValueStore: mockKeyValueStore); var stream = hydratedSubject.distinct(); expect(stream, emitsInOrder([1, 2])); @@ -1057,7 +1057,7 @@ void main() { test('timeout', () { { - var hydratedSubject = HydratedSubject('key', seedValue: 1, persistence: mockKeyValueStore); + var hydratedSubject = HydratedSubject('key', seedValue: 1, keyValueStore: mockKeyValueStore); var stream = hydratedSubject .interval(const Duration(milliseconds: 100)) @@ -1078,7 +1078,7 @@ void main() { { var hydratedSubject = - HydratedSubject('key', persistence: mockKeyValueStore); + HydratedSubject('key', keyValueStore: mockKeyValueStore); var stream = hydratedSubject .interval(const Duration(milliseconds: 100)) diff --git a/test/hydrated_test.dart b/test/hydrated_test.dart index 11874b6..c33074c 100644 --- a/test/hydrated_test.dart +++ b/test/hydrated_test.dart @@ -51,7 +51,7 @@ void main() { test('Tries to hydrate upon instantiation', () { mockKeyValueStore.getOverride = expectAsync1((_) async {}, count: 1); - HydratedSubject(key, persistence: mockKeyValueStore); + HydratedSubject(key, keyValueStore: mockKeyValueStore); }); test( @@ -59,7 +59,7 @@ void main() { () { mockKeyValueStore.getOverride = (_) async => 42; final subject = - HydratedSubject(key, persistence: mockKeyValueStore); + HydratedSubject(key, keyValueStore: mockKeyValueStore); expect(subject, emits(42)); }); @@ -68,7 +68,7 @@ void main() { () { mockKeyValueStore.getOverride = (_) async => null; final subject = - HydratedSubject(key, persistence: mockKeyValueStore); + HydratedSubject(key, keyValueStore: mockKeyValueStore); expect(subject, neverEmits(anything)); subject.close(); @@ -79,7 +79,7 @@ void main() { () { expect(() { HydratedSubject(key, - persistence: mockKeyValueStore, hydrate: (_) => 1); + keyValueStore: mockKeyValueStore, hydrate: (_) => 1); }, throwsA(isA())); }); @@ -88,7 +88,7 @@ void main() { () { expect(() { HydratedSubject(key, - persistence: mockKeyValueStore, persist: (_) => ''); + keyValueStore: mockKeyValueStore, persist: (_) => ''); }, throwsA(isA())); }); @@ -104,7 +104,7 @@ void main() { final subject = HydratedSubject( key, - persistence: mockKeyValueStore, + keyValueStore: mockKeyValueStore, hydrate: hydrateCallback, persist: (_) => '', ); @@ -114,7 +114,7 @@ void main() { }); test('exposes the persistence key', () { - final subject = HydratedSubject(key, persistence: mockKeyValueStore); + final subject = HydratedSubject(key, keyValueStore: mockKeyValueStore); expect(subject.key, key); }); @@ -125,7 +125,7 @@ void main() { mockKeyValueStore.putOverride = expectAsync2((key, value) async { expect(value, equals(testValue)); }, count: 1); - final subject = HydratedSubject(key, persistence: mockKeyValueStore); + final subject = HydratedSubject(key, keyValueStore: mockKeyValueStore); subject.add(testValue); }); @@ -147,7 +147,7 @@ void main() { }, count: 1); final subject = HydratedSubject( key, - persistence: mockKeyValueStore, + keyValueStore: mockKeyValueStore, hydrate: (_) => 1, persist: persistCallback, ); @@ -160,11 +160,11 @@ void main() { 'given persistence interface `get` throws a PersistenceError, ' 'it emits the error through the stream', () { mockKeyValueStore.getOverride = - (_) async => throw PersistenceError('test'); + (_) async => throw StoreError('test'); final subject = - HydratedSubject(key, persistence: mockKeyValueStore); + HydratedSubject(key, keyValueStore: mockKeyValueStore); - expect(subject, emitsError(isA())); + expect(subject, emitsError(isA())); }); test( @@ -177,7 +177,7 @@ void main() { final completer = Completer(); HydratedSubject( key, - persistence: mockKeyValueStore, + keyValueStore: mockKeyValueStore, onHydrate: completer.complete, ); return completer.future; @@ -193,15 +193,15 @@ void main() { 'it emits the error through the stream', () async { const testValue = 42; mockKeyValueStore.putOverride = - (_, __) => throw PersistenceError('test'); + (_, __) => throw StoreError('test'); final subject = - HydratedSubject(key, persistence: mockKeyValueStore); + HydratedSubject(key, keyValueStore: mockKeyValueStore); final expectation = expectLater( subject, emitsInOrder([ 42, - emitsError(isA()), + emitsError(isA()), ])); subject.add(testValue); diff --git a/test/shared_preferences_store_test.dart b/test/shared_preferences_store_test.dart index d3fdf25..689deae 100644 --- a/test/shared_preferences_store_test.dart +++ b/test/shared_preferences_store_test.dart @@ -55,7 +55,7 @@ void main() { _setMockPersistedValue('key', unsupportedTypeValue); expect( () => SharedPreferencesStore().get('key'), - throwsA(isA()), + throwsA(isA()), ); }); }); From 23a750eb58f1959b875404c0771c52534f7ff73b Mon Sep 17 00:00:00 2001 From: Yurii Prykhodko Date: Thu, 26 Aug 2021 15:22:45 +0300 Subject: [PATCH 21/21] Finish the rename, use StoreError.unsupportedType, fix test names --- .../shared_preferences_store.dart | 33 ++++++++++--------- lib/src/key_value_store/store_error.dart | 10 +++++- test/hydrated_test.dart | 4 +-- test/shared_preferences_store_test.dart | 14 ++++---- 4 files changed, 36 insertions(+), 25 deletions(-) diff --git a/lib/src/key_value_store/shared_preferences_store.dart b/lib/src/key_value_store/shared_preferences_store.dart index 6007631..811a989 100644 --- a/lib/src/key_value_store/shared_preferences_store.dart +++ b/lib/src/key_value_store/shared_preferences_store.dart @@ -19,7 +19,7 @@ class SharedPreferencesStore implements KeyValueStore { @override Future get(String key) async { - _assertSupportedType(); + _ensureSupportedType(); final prefs = await _getPrefs(); T? val; @@ -47,7 +47,7 @@ class SharedPreferencesStore implements KeyValueStore { @override Future put(String key, T? value) async { - _assertSupportedType(); + _ensureSupportedType(); final prefs = await _getPrefs(); if (value == null) @@ -63,19 +63,22 @@ class SharedPreferencesStore implements KeyValueStore { else if (value is List) await prefs.setStringList(key, value); } - void _assertSupportedType() { - assert( - _areTypesEqual() || - _areTypesEqual() || - _areTypesEqual() || - _areTypesEqual() || - _areTypesEqual() || - _areTypesEqual() || - _areTypesEqual() || - _areTypesEqual() || - _areTypesEqual>() || - _areTypesEqual?>(), - '$T type is not supported by SharedPreferences.'); + void _ensureSupportedType() { + if (_areTypesEqual() || + _areTypesEqual() || + _areTypesEqual() || + _areTypesEqual() || + _areTypesEqual() || + _areTypesEqual() || + _areTypesEqual() || + _areTypesEqual() || + _areTypesEqual>() || + _areTypesEqual?>()) { + return; + } else { + throw StoreError.unsupportedType( + '$T type is not supported by SharedPreferences.'); + } } Future _getPrefs() => SharedPreferences.getInstance(); diff --git a/lib/src/key_value_store/store_error.dart b/lib/src/key_value_store/store_error.dart index 4aad0e4..ae54238 100644 --- a/lib/src/key_value_store/store_error.dart +++ b/lib/src/key_value_store/store_error.dart @@ -1,8 +1,16 @@ /// An error encountered when persisting a value, or restoring it from persistence. +/// +/// This is probably a configuration error -- check the `KeyValueStore` +/// implementation and `HydratedSubject` `persist` and `hydrate` callbacks +/// for type compatibility. class StoreError extends Error { /// A description of an error. final String message; - /// A persistence error with a [message] describing its details. + /// A storage error with a [message] describing its details. StoreError(this.message); + + /// A storage has encountered an unsupported type. + StoreError.unsupportedType(String message) + : message = 'Error storing an unsupported type: $message'; } diff --git a/test/hydrated_test.dart b/test/hydrated_test.dart index c33074c..eb89354 100644 --- a/test/hydrated_test.dart +++ b/test/hydrated_test.dart @@ -157,7 +157,7 @@ void main() { group('persistence error handling', () { test( - 'given persistence interface `get` throws a PersistenceError, ' + 'given persistence interface `get` throws a StoreError, ' 'it emits the error through the stream', () { mockKeyValueStore.getOverride = (_) async => throw StoreError('test'); @@ -189,7 +189,7 @@ void main() { }); test( - 'given persistence interface put throws a PersistenceError, ' + 'given persistence interface put throws a StoreError, ' 'it emits the error through the stream', () async { const testValue = 42; mockKeyValueStore.putOverride = diff --git a/test/shared_preferences_store_test.dart b/test/shared_preferences_store_test.dart index 689deae..102107b 100644 --- a/test/shared_preferences_store_test.dart +++ b/test/shared_preferences_store_test.dart @@ -21,35 +21,35 @@ void main() { group('SharedPreferencesStore', () { group('handles unsupported types', () { test( - 'when saving a value with an unsupported type, it throws an AssertionError', + 'when saving a value with an unsupported type, it throws a StoreError', () { final unsupportedTypeValue = Exception('test unsupported value'); expect( () => SharedPreferencesStore().put('key', unsupportedTypeValue), - throwsA(isA()), + throwsA(isA()), ); }); test( - 'when getting a value with an unspecified type (dynamic), it throws an AssertionError', + 'when getting a value with an unspecified type (dynamic), it throws an StoreError', () { expect( () => SharedPreferencesStore().get('key'), - throwsA(isA()), + throwsA(isA()), ); }); test( - 'when getting a value with an unsupported type, it throws an AssertionError', + 'when getting a value with an unsupported type, it throws an StoreError', () { expect( () => SharedPreferencesStore().get('key'), - throwsA(isA()), + throwsA(isA()), ); }); test( - 'when SharedPreferences return an unsupported type, it throws a PersistenceError', + 'when SharedPreferences return an unsupported type, it throws a StoreError', () { final unsupportedTypeValue = Exception('test unsupported value'); _setMockPersistedValue('key', unsupportedTypeValue);