Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for generic persistence interface -- alternative 1 #27

Merged
merged 21 commits into from Aug 26, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
b0021dd
Add support for a generic persistence interface
solid-yuriiprykhodko Aug 18, 2021
60a5bc2
Add a different constructor for using a custom storage, revert breaki…
solid-yuriiprykhodko Aug 19, 2021
ba44dcb
Keep key, hydrate and persist callbacks in the HydratedSubject
solid-yuriiprykhodko Aug 19, 2021
dfb8401
Rename GenericValuePersistence -> KeyValueStore
solid-yuriiprykhodko Aug 19, 2021
de48f0b
Make KeyValueStore.put always accept nullables.
solid-yuriiprykhodko Aug 19, 2021
da6ad16
Consistently use local imports
solid-yuriiprykhodko Aug 19, 2021
55ac46b
Add missing newline
solid-yuriiprykhodko Aug 19, 2021
e3728f3
Restore `key` getter
solid-yuriiprykhodko Aug 23, 2021
41f9310
Readability improvements
solid-yuriiprykhodko Aug 23, 2021
986b272
Add code docs
solid-yuriiprykhodko Aug 23, 2021
8e6843e
Bring in BehaviorSubject test suite from rxdart repo
solid-yuriiprykhodko Aug 23, 2021
e5910c9
Add HydratedSubject tests (WIP)
solid-yuriiprykhodko Aug 23, 2021
2c0b827
Remove the use of `!` ("bang") operator
solid-yuriiprykhodko Aug 26, 2021
d616400
More unit-tests for HydratedSubject
solid-yuriiprykhodko Aug 26, 2021
7c45d85
More persistence unit-tests
solid-yuriiprykhodko Aug 26, 2021
6d69263
Ignore VSCode files and coverage dir
solid-yuriiprykhodko Aug 26, 2021
e0abcde
Remove unused class, fix typo
solid-yuriiprykhodko Aug 26, 2021
cb45bca
Remove unused imports
solid-yuriiprykhodko Aug 26, 2021
b7695d5
Rename SharedPreferencesPersistence -> SharedPreferencesStore
solid-yuriiprykhodko Aug 26, 2021
8459c21
Rename PersistenceError -> StoreError
solid-yuriiprykhodko Aug 26, 2021
23a750e
Finish the rename, use StoreError.unsupportedType, fix test names
solid-yuriiprykhodko Aug 26, 2021
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
3 changes: 2 additions & 1 deletion .gitignore
Expand Up @@ -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/
Expand All @@ -29,6 +29,7 @@
.pub-cache/
.pub/
build/
coverage/

# Android related
**/android/**/gradle-wrapper.jar
Expand Down
3 changes: 3 additions & 0 deletions lib/hydrated.dart
@@ -1,3 +1,6 @@
library hydrated;

export 'src/hydrated.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';
145 changes: 51 additions & 94 deletions lib/src/hydrated.dart
@@ -1,9 +1,11 @@
import 'dart:async';

import 'package:flutter/foundation.dart';
import 'package:hydrated/src/utils/type_utils.dart';
import 'package:rxdart/rxdart.dart';
import 'package:shared_preferences/shared_preferences.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<T> = String? Function(T);
Expand All @@ -15,21 +17,8 @@ typedef HydrateCallback<T> = T Function(String);
///
/// Mimics the behavior of a [BehaviorSubject].
///
/// HydratedSubject supports serialized classes and [shared_preferences] types
/// such as:
/// - `int`
/// - `double`
/// - `bool`
/// - `String`
/// - `List<String>`.
///
/// 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 [KeyValueStore] implementation.
/// For a list of types supported by default see [SharedPreferencesStore].
///
/// Example:
///
Expand Down Expand Up @@ -58,21 +47,27 @@ typedef HydrateCallback<T> = T Function(String);
/// );
/// ```
class HydratedSubject<T> extends Subject<T> implements ValueStream<T> {
static final _areTypesEqual = TypeUtils.areTypesEqual;
final BehaviorSubject<T> _subject;
final String _key;
final HydrateCallback<T>? _hydrate;
final PersistCallback<T>? _persist;
final BehaviorSubject<T> _subject;
final VoidCallback? _onHydrate;
final T? _seedValue;

illia-romanenko marked this conversation as resolved.
Show resolved Hide resolved
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,
this._hydrate,
this._persist,
this._onHydrate,
this._subject,
this._persistence,
) : super(_subject, _subject.stream) {
_hydrateSubject();
}
Expand All @@ -85,22 +80,13 @@ class HydratedSubject<T> extends Subject<T> implements ValueStream<T> {
VoidCallback? onHydrate,
VoidCallback? onListen,
VoidCallback? onCancel,
bool sync: false,
bool sync = false,
KeyValueStore keyValueStore = const SharedPreferencesStore(),
}) {
// assert that T is a type compatible with shared_preferences,
// or that we have hydrate and persist mapping functions
assert(_areTypesEqual<T, int>() ||
_areTypesEqual<T, int?>() ||
_areTypesEqual<T, double>() ||
_areTypesEqual<T, double?>() ||
_areTypesEqual<T, bool>() ||
_areTypesEqual<T, bool?>() ||
_areTypesEqual<T, String>() ||
_areTypesEqual<T, String?>() ||
_areTypesEqual<T, List<String>>() ||
_areTypesEqual<T, List<String>?>() ||
(hydrate != null && persist != null));

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<T>.seeded(
Expand All @@ -122,16 +108,12 @@ class HydratedSubject<T> extends Subject<T> implements ValueStream<T> {
persist,
onHydrate,
subject,
keyValueStore,
);
}

/// A unique key that references a storage container
/// for a value persisted on the device.
String get key => _key;

solid-yuriiprykhodko marked this conversation as resolved.
Show resolved Hide resolved
@override
void onAdd(T event) {
_subject.add(event);
_persistValue(event);
}

Expand All @@ -149,7 +131,7 @@ class HydratedSubject<T> extends Subject<T> implements ValueStream<T> {
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;
Expand All @@ -167,68 +149,43 @@ class HydratedSubject<T> extends Subject<T> implements ValueStream<T> {
///
/// Must be called to retrieve values stored on the device.
Future<void> _hydrateSubject() async {
final prefs = await SharedPreferences.getInstance();

T? val;
try {
T? val;
final hydrate = _hydrate;
if (hydrate != null) {
final persistedValue = await _persistence.get<String>(_key);
if (persistedValue != null) {
val = hydrate(persistedValue);
}
} else {
val = await _persistence.get<T?>(_key);
}

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<T, int>() || _areTypesEqual<T, int?>())
val = prefs.getInt(_key) as T?;
else if (_areTypesEqual<T, double>() || _areTypesEqual<T, double?>())
val = prefs.getDouble(_key) as T?;
else if (_areTypesEqual<T, bool>() || _areTypesEqual<T, bool?>())
val = prefs.getBool(_key) as T?;
else if (_areTypesEqual<T, String>() || _areTypesEqual<T, String?>())
val = prefs.getString(_key) as T?;
else if (_areTypesEqual<T, List<String>>() ||
_areTypesEqual<T, List<String>?>())
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 StoreError 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<String>)
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);
try {
final persist = _persist;
var persistedVal;
if (persist != null) {
persistedVal = persist(val);
await _persistence.put<String>(_key, persistedVal);
} else {
prefs.remove(_key);
persistedVal = val;
await _persistence.put<T>(_key, persistedVal);
}
} else {
final error = Exception(
'HydratedSubject – value must be int, '
'double, bool, String, or List<String>',
);
_subject.addError(error, StackTrace.current);
} on StoreError catch (e, s) {
addError(e, s);
}
}

Expand Down
16 changes: 16 additions & 0 deletions lib/src/key_value_store/key_value_store.dart
@@ -0,0 +1,16 @@
import 'store_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 [StoreError] if encountering a problem while persisting a value.
Future<void> put<T>(String key, T? value);

/// Retrieve a value from persistence.
///
/// Throw a [StoreError] if encountering a problem while restoring a value from the storage.
Future<T?> get<T>(String key);
}
85 changes: 85 additions & 0 deletions lib/src/key_value_store/shared_preferences_store.dart
@@ -0,0 +1,85 @@
import 'package:shared_preferences/shared_preferences.dart';

import '../utils/type_utils.dart';
import 'key_value_store.dart';
import 'store_error.dart';

/// An adapter for [SharedPreferences] persistence.
///
/// Supported types:
/// - `int`
/// - `double`
/// - `bool`
/// - `String`
/// - `List<String>`.
class SharedPreferencesStore implements KeyValueStore {
static final _areTypesEqual = TypeUtils.areTypesEqual;

const SharedPreferencesStore();

@override
Future<T?> get<T>(String key) async {
_ensureSupportedType<T>();
final prefs = await _getPrefs();

T? val;

try {
if (_areTypesEqual<T, int>() || _areTypesEqual<T, int?>())
val = prefs.getInt(key) as T?;
else if (_areTypesEqual<T, double>() || _areTypesEqual<T, double?>())
val = prefs.getDouble(key) as T?;
else if (_areTypesEqual<T, bool>() || _areTypesEqual<T, bool?>())
val = prefs.getBool(key) as T?;
else if (_areTypesEqual<T, String>() || _areTypesEqual<T, String?>())
val = prefs.getString(key) as T?;
else if (_areTypesEqual<T, List<String>>() ||
_areTypesEqual<T, List<String>?>())
val = prefs.getStringList(key) as T?;
} catch (e) {
throw StoreError(
'Error retrieving value from SharedPreferences: $e',
);
}

return val;
}

@override
Future<void> put<T>(String key, T? value) async {
_ensureSupportedType<T>();
final prefs = await _getPrefs();

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);
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<String>) await prefs.setStringList(key, value);
}

void _ensureSupportedType<T>() {
if (_areTypesEqual<T, int>() ||
_areTypesEqual<T, int?>() ||
_areTypesEqual<T, double>() ||
_areTypesEqual<T, double?>() ||
_areTypesEqual<T, bool>() ||
_areTypesEqual<T, bool?>() ||
_areTypesEqual<T, String>() ||
_areTypesEqual<T, String?>() ||
_areTypesEqual<T, List<String>>() ||
_areTypesEqual<T, List<String>?>()) {
return;
} else {
throw StoreError.unsupportedType(
'$T type is not supported by SharedPreferences.');
}
}

Future<SharedPreferences> _getPrefs() => SharedPreferences.getInstance();
}
16 changes: 16 additions & 0 deletions lib/src/key_value_store/store_error.dart
@@ -0,0 +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 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';
}
10 changes: 10 additions & 0 deletions 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<int, int>() == true
/// TypeUtils.areTypesEqual<int, int?>() == false
/// ```
static bool areTypesEqual<T1, T2>() {
return T1 == T2;
}
Expand Down