From 047f01ec23b0ad7d862307438020998c27cbf710 Mon Sep 17 00:00:00 2001 From: Yuri Prykhodko <40931732+solid-yuriiprykhodko@users.noreply.github.com> Date: Mon, 9 Aug 2021 15:19:35 +0300 Subject: [PATCH] feat: Migrate package to use NNBD and rxdart ^0.27.0 (#21) * Migrate package to use NNBD * Update plugin_platform_interface dependency version * Bump version to 1.3.0-nullsafety.1 * Use channel beta for the Github Actions * Fix deprecations and warnings * Update to rxdart ^0.27.0, refactor implementation * Use Flutter channel `stable` for CI * Update lower Dart SDK constraint to `2.12`, remove old analysis options * Don't explicitly initialise to `null` * Fix tests * Update homepage * Update gitignore, exclude IDE files * Migrate example app to NNBD, update its pubspec * Add SubjectValueWrapper * Make the _persist callback operate on the original type * Reformat * Adapt type checks to null-safety * Documentation improvements * Use a list instead of prose in HydratedSubject doc comment Co-authored-by: solid-vovabeloded <41615621+solid-vovabeloded@users.noreply.github.com> * Improve HydratedSubject doc comment readability Co-authored-by: solid-vovabeloded <41615621+solid-vovabeloded@users.noreply.github.com> * Remove `late` modifier in tests, remove unnecessary null check * Set value wrapper when hydrating * Fix wrapper not being set * Fix value setter * Fix hydration type casts * Fix value removal for primitive types * Autoformat * Use dedicated matchers * Make widget members private in example app * Move subject_value_wrapper.dart to src/model * Move implementation into `src` * Add export file * Move variables declaration list to the top of class declaration * Extract type check function into a utility class * Use a typedef for function declaration * Remove TypeComparisonFunction typedef, fix util class naming Co-authored-by: Yurii Prykhodko Co-authored-by: solid-vovabeloded <41615621+solid-vovabeloded@users.noreply.github.com> --- .github/workflows/flutter.yaml | 1 - .gitignore | 75 ++++++- .idea/libraries/Dart_SDK.xml | 19 -- .idea/modules.xml | 8 - .idea/workspace.xml | 36 ---- example/lib/class_example.dart | 48 +++-- example/lib/main.dart | 44 ++-- example/pubspec.yaml | 47 +--- hydrated.iml | 19 -- lib/hydrated.dart | 188 +--------------- lib/src/hydrated.dart | 263 +++++++++++++++++++++++ lib/src/model/subject_value_wrapper.dart | 11 + lib/src/utils/typing_utils.dart | 5 + pubspec.lock | 146 ++++++++++--- pubspec.yaml | 10 +- test/hydrated_test.dart | 38 ++-- 16 files changed, 550 insertions(+), 408 deletions(-) delete mode 100644 .idea/libraries/Dart_SDK.xml delete mode 100644 .idea/modules.xml delete mode 100644 .idea/workspace.xml delete mode 100644 hydrated.iml create mode 100644 lib/src/hydrated.dart create mode 100644 lib/src/model/subject_value_wrapper.dart create mode 100644 lib/src/utils/typing_utils.dart diff --git a/.github/workflows/flutter.yaml b/.github/workflows/flutter.yaml index 4224495..602b53e 100644 --- a/.github/workflows/flutter.yaml +++ b/.github/workflows/flutter.yaml @@ -9,7 +9,6 @@ jobs: - uses: actions/checkout@v1 - uses: subosito/flutter-action@v1 with: - flutter-version: '1.12.13+hotfix.5' channel: 'stable' - run: flutter pub get - run: flutter analyze diff --git a/.gitignore b/.gitignore index 446ed0d..a247422 100644 --- a/.gitignore +++ b/.gitignore @@ -1,10 +1,75 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp .DS_Store -.dart_tool/ +.atom/ +.buildlog/ +.history +.svn/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ +# 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/ + +# Flutter/Dart/Pub related +**/doc/api/ +.dart_tool/ +.flutter-plugins +.flutter-plugins-dependencies .packages +.pub-cache/ .pub/ - build/ -ios/.generated/ -ios/Flutter/Generated.xcconfig -ios/Runner/GeneratedPluginRegistrant.* + +# Android related +**/android/**/gradle-wrapper.jar +**/android/.gradle +**/android/captures/ +**/android/gradlew +**/android/gradlew.bat +**/android/local.properties +**/android/**/GeneratedPluginRegistrant.java + +# iOS/XCode related +**/ios/**/*.mode1v3 +**/ios/**/*.mode2v3 +**/ios/**/*.moved-aside +**/ios/**/*.pbxuser +**/ios/**/*.perspectivev3 +**/ios/**/*sync/ +**/ios/**/.sconsign.dblite +**/ios/**/.tags* +**/ios/**/.vagrant/ +**/ios/**/DerivedData/ +**/ios/**/Icon? +**/ios/**/Pods/ +**/ios/**/.symlinks/ +**/ios/**/profile +**/ios/**/xcuserdata +**/ios/.generated/ +**/ios/Flutter/App.framework +**/ios/Flutter/Flutter.framework +**/ios/Flutter/Flutter.podspec +**/ios/Flutter/Generated.xcconfig +**/ios/Flutter/ephemeral +**/ios/Flutter/app.flx +**/ios/Flutter/app.zip +**/ios/Flutter/flutter_assets/ +**/ios/Flutter/flutter_export_environment.sh +**/ios/ServiceDefinitions.json +**/ios/Runner/GeneratedPluginRegistrant.* + +# Exceptions to above rules. +!**/ios/**/default.mode1v3 +!**/ios/**/default.mode2v3 +!**/ios/**/default.pbxuser +!**/ios/**/default.perspectivev3 diff --git a/.idea/libraries/Dart_SDK.xml b/.idea/libraries/Dart_SDK.xml deleted file mode 100644 index 896efb4..0000000 --- a/.idea/libraries/Dart_SDK.xml +++ /dev/null @@ -1,19 +0,0 @@ - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml deleted file mode 100644 index b25ed89..0000000 --- a/.idea/modules.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - diff --git a/.idea/workspace.xml b/.idea/workspace.xml deleted file mode 100644 index 5b3388c..0000000 --- a/.idea/workspace.xml +++ /dev/null @@ -1,36 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/example/lib/class_example.dart b/example/lib/class_example.dart index 3f554ae..25bcd34 100644 --- a/example/lib/class_example.dart +++ b/example/lib/class_example.dart @@ -17,16 +17,20 @@ class MyApp extends StatelessWidget { } class MyHomePage extends StatelessWidget { - final String title; + final String _title; - final count$ = HydratedSubject( + final _countSubject = HydratedSubject( "serialized-count", hydrate: (value) => SerializedClass.fromJSON(value), persist: (value) => value.toJSON, seedValue: SerializedClass(0), ); - MyHomePage({Key key, this.title}) : super(key: key); + MyHomePage({ + Key? key, + required String title, + }) : _title = title, + super(key: key); @override Widget build(BuildContext context) { @@ -34,42 +38,42 @@ class MyHomePage extends StatelessWidget { return Scaffold( appBar: AppBar( - title: Text(this.title), + title: Text(this._title), ), body: Center( child: StreamBuilder( - stream: count$, - initialData: count$.value, - builder: (context, snap) => Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text( - 'You have pushed the button this many times:', - ), - Text( - '${snap.data.count}', - style: Theme.of(context).textTheme.display1, - ), - ], + stream: _countSubject, + initialData: _countSubject.value, + builder: (context, snapshot) => Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text('You have pushed the button this many times:'), + Text( + '${snapshot.data?.count}', + style: Theme.of(context).textTheme.headline4, ), + ], + ), ), ), floatingActionButton: FloatingActionButton( - onPressed: () { - final count = count$.value.count + 1; - count$.add(SerializedClass(count)); - }, + onPressed: _incrementCounter, tooltip: 'Increment', child: Icon(Icons.add), ), ); } + + void _incrementCounter() { + final count = _countSubject.value.count + 1; + _countSubject.add(SerializedClass(count)); + } } class SerializedClass { final int count; - SerializedClass(this.count); + const SerializedClass(this.count); SerializedClass.fromJSON(String json) : this.count = int.parse(json); diff --git a/example/lib/main.dart b/example/lib/main.dart index bdbe909..759e904 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -17,42 +17,50 @@ class MyApp extends StatelessWidget { } class MyHomePage extends StatelessWidget { - final String title; - final count$ = HydratedSubject("count", seedValue: 0); + final String _title; + final _count = HydratedSubject("count", seedValue: 0); - MyHomePage({Key key, this.title}) : super(key: key); + MyHomePage({ + Key? key, + required String title, + }) : _title = title, + super(key: key); @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( - title: Text(this.title), + title: Text(this._title), ), body: Center( child: StreamBuilder( - stream: count$, - initialData: count$.value, + stream: _count, + initialData: _count.value, builder: (context, snap) => Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text( - 'You have pushed the button this many times:', - ), - Text( - '${snap.data}', - style: Theme.of(context).textTheme.display1, - ), - ], + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text('You have pushed the button this many times:'), + Text( + '${snap.data}', + style: Theme.of(context).textTheme.headline4, ), + ], + ), ), ), floatingActionButton: FloatingActionButton( - onPressed: () => count$.value++, + onPressed: _incrementCounter, tooltip: 'Increment', child: Icon(Icons.add), ), ); } - void dispose() => count$.close(); + void _incrementCounter() { + _count.value++; + } + + void dispose() { + _count.close(); + } } diff --git a/example/pubspec.yaml b/example/pubspec.yaml index 96458b6..92d0015 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -1,65 +1,22 @@ name: hydrated_demo description: A demo showcasing the Hydrated package. +publish_to: 'none' -# The following defines the version and build number for your application. -# A version number is three numbers separated by dots, like 1.2.43 -# followed by an optional build number separated by a +. -# Both the version and the builder number may be overridden in flutter -# build by specifying --build-name and --build-number, respectively. -# Read more about versioning at semver.org. version: 1.0.0+1 environment: - sdk: ">=2.6.0 <3.0.0" + sdk: ">=2.12.0 <3.0.0" dependencies: flutter: sdk: flutter - # The following adds the Cupertino Icons font to your application. - # Use with the CupertinoIcons class for iOS style icons. - cupertino_icons: ^0.1.2 hydrated: path: ../ - dev_dependencies: flutter_test: sdk: flutter -# For information on the generic Dart part of this file, see the -# following page: https://www.dartlang.org/tools/pub/pubspec - -# The following section is specific to Flutter. flutter: - # The following line ensures that the Material Icons font is - # included with your application, so that you can use the icons in - # the material Icons class. uses-material-design: true - # To add assets to your application, add an assets section, like this: - # assets: - # - images/a_dot_burr.jpeg - # - images/a_dot_ham.jpeg - # An image asset can refer to one or more resolution-specific "variants", see - # https://flutter.io/assets-and-images/#resolution-aware. - # For details regarding adding assets from package dependencies, see - # https://flutter.io/assets-and-images/#from-packages - # To add custom fonts to your application, add a fonts section here, - # in this "flutter" section. Each entry in this list should have a - # "family" key with the font family name, and a "fonts" key with a - # list giving the asset and other descriptors for the font. For - # example: - # fonts: - # - family: Schyler - # fonts: - # - asset: fonts/Schyler-Regular.ttf - # - asset: fonts/Schyler-Italic.ttf - # style: italic - # - family: Trajan Pro - # fonts: - # - asset: fonts/TrajanPro.ttf - # - asset: fonts/TrajanPro_Bold.ttf - # weight: 700 - # - # For details regarding fonts from package dependencies, - # see https://flutter.io/custom-fonts/#from-packages diff --git a/hydrated.iml b/hydrated.iml deleted file mode 100644 index 8d48a06..0000000 --- a/hydrated.iml +++ /dev/null @@ -1,19 +0,0 @@ - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/lib/hydrated.dart b/lib/hydrated.dart index ba0fc62..4a3011e 100644 --- a/lib/hydrated.dart +++ b/lib/hydrated.dart @@ -1,189 +1,3 @@ library hydrated; -import 'dart:async'; -import 'package:shared_preferences/shared_preferences.dart'; -import 'package:rxdart/rxdart.dart'; - -/// A [Subject] that automatically persists its values and hydrates on creation. -/// -/// HydratedSubject supports serialized classes and [shared_preferences] types such as: `int`, `double`, `bool`, `String`, and `List` -/// -/// Serialized classes are supported by using the `hydrate: (String)=>Class` and `persist: (Class)=>String` constructor arguments. -/// -/// Example: -/// -/// ``` -/// final count$ = HydratedSubject("count", seedValue: 0); -/// ``` -/// -/// Serialized class example: -/// -/// ``` -/// final user$ = HydratedSubject( -/// "user", -/// hydrate: (String s) => User.fromJSON(s), -/// persist: (User user) => user.toJSON(), -/// seedValue: User.empty(), -/// ); -/// ``` -/// -/// Hydration is performed automatically and is asynchronous. The `onHydrate` callback is called when hydration is complete. -/// -/// ``` -/// final user$ = HydratedSubject( -/// "count", -/// onHydrate: () => loading$.add(false), -/// ); -/// ``` - -class HydratedSubject extends Subject implements ValueStream { - String _key; - T _seedValue; - _Wrapper _wrapper; - - T Function(String value) _hydrate; - String Function(T value) _persist; - void Function() _onHydrate; - - HydratedSubject._( - this._key, - this._seedValue, - this._hydrate, - this._persist, - this._onHydrate, - StreamController controller, - Stream observable, - this._wrapper, - ) : super(controller, observable) { - _hydrateSubject(); - } - - factory HydratedSubject( - String key, { - T seedValue, - T Function(String value) hydrate, - String Function(T value) persist, - void onHydrate(), - void onListen(), - void onCancel(), - bool sync: false, - }) { - // assert that T is a type compatible with shared_preferences, - // or that we have hydrate and persist mapping functions - assert(T == int || - T == double || - T == bool || - T == String || - [""] is T || - (hydrate != null && persist != null)); - - // ignore: close_sinks - final controller = new StreamController.broadcast( - onListen: onListen, - onCancel: onCancel, - sync: sync, - ); - - final wrapper = new _Wrapper(seedValue); - - return new HydratedSubject._( - key, - seedValue, - hydrate, - persist, - onHydrate, - controller, - Rx.defer( - () => wrapper.latestValue == null - ? controller.stream - : controller.stream - .startWith(wrapper.latestValue), - reusable: true), - wrapper); - } - - @override - void onAdd(T event) { - _wrapper.latestValue = event; - _persistValue(event); - } - - @override - ValueStream get stream => this; - - /// Get the latest value emitted by the Subject - @override - T get value => _wrapper.latestValue; - - /// Set and emit the new value - set value(T newValue) => add(newValue); - - @override - bool get hasValue => _wrapper.latestValue != null; - - /// Hydrates the HydratedSubject with a value stored on the user's device. - /// - /// Must be called to retreive values stored on the device. - Future _hydrateSubject() async { - final prefs = await SharedPreferences.getInstance(); - - var val; - - if (this._hydrate != null) - val = this._hydrate(prefs.getString(this._key)); - else if (T == int) - val = prefs.getInt(this._key); - else if (T == double) - val = prefs.getDouble(this._key); - else if (T == bool) - val = prefs.getBool(this._key); - else if (T == String) - val = prefs.getString(this._key); - else if ([""] is T) - val = prefs.getStringList(this._key); - 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) { - add(val); - } - - if (_onHydrate != null) { - this._onHydrate(); - } - } - - _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 (this._persist != null) - await prefs.setString(_key, this._persist(val)); - else - Exception( - "HydratedSubject – value must be int, double, bool, String, or List", - ); - } - - /// A unique key that references a storage container for a value persisted on the device. - String get key => this._key; -} - -class _Wrapper { - T latestValue; - - _Wrapper(this.latestValue); -} +export 'src/hydrated.dart'; \ No newline at end of file diff --git a/lib/src/hydrated.dart b/lib/src/hydrated.dart new file mode 100644 index 0000000..117c009 --- /dev/null +++ b/lib/src/hydrated.dart @@ -0,0 +1,263 @@ +import 'dart:async'; + +import 'package:flutter/foundation.dart'; +import 'package:hydrated/src/utils/typing_utils.dart'; +import 'package:rxdart/rxdart.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +import 'model/subject_value_wrapper.dart'; + +/// A callback for encoding an instance of a data class into a String. +typedef PersistCallback = String? Function(T); + +/// A callback for reconstructing an instance of a data class from a String. +typedef HydrateCallback = T Function(String); + +/// A [Subject] that automatically persists its values and hydrates on creation. +/// +/// 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 +/// ``` +/// +/// Example: +/// +/// ``` +/// final count = HydratedSubject("count", seedValue: 0); +/// ``` +/// +/// Serialized class example: +/// +/// ``` +/// final user = HydratedSubject( +/// "user", +/// hydrate: (String s) => User.fromJSON(s), +/// persist: (User user) => user.toJSON(), +/// seedValue: User.empty(), +/// ); +/// ``` +/// +/// Hydration is performed automatically and is asynchronous. +/// The `onHydrate` callback is called when hydration is complete. +/// +/// ``` +/// final user = HydratedSubject( +/// "count", +/// onHydrate: () => loading.add(false), +/// ); +/// ``` +class HydratedSubject extends Subject implements ValueStream { + static final _areTypesEqual = TypeUtils.areTypesEqual; + + final String _key; + final HydrateCallback? _hydrate; + final PersistCallback? _persist; + final VoidCallback? _onHydrate; + final T? _seedValue; + final StreamController _controller; + + SubjectValueWrapper? _wrapper; + + HydratedSubject._( + this._key, + this._seedValue, + this._hydrate, + this._persist, + this._onHydrate, + this._controller, + Stream observable, + this._wrapper, + ) : super(_controller, observable) { + _hydrateSubject(); + } + + factory HydratedSubject( + String key, { + T? seedValue, + HydrateCallback? hydrate, + PersistCallback? persist, + 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)); + + // ignore: close_sinks + final StreamController controller = StreamController.broadcast( + onListen: onListen, + onCancel: onCancel, + sync: sync, + ); + + final wrapper = + seedValue != null ? SubjectValueWrapper(value: seedValue) : null; + + return HydratedSubject._( + key, + seedValue, + hydrate, + persist, + onHydrate, + controller, + Rx.defer( + () => wrapper == null + ? controller.stream + : controller.stream.startWith(wrapper.value!), + reusable: true), + wrapper); + } + + /// A unique key that references a storage container + /// for a value persisted on the device. + String get key => _key; + + @override + void onAdd(T event) { + _wrapper = SubjectValueWrapper(value: event); + _persistValue(event); + } + + @override + ValueStream get stream => this; + + @override + bool get hasValue => _wrapper?.value != null; + + @override + T? get valueOrNull => _wrapper?.value; + + /// Get the latest value emitted by the Subject + @override + T get value => + hasValue ? _wrapper!.value! : throw ValueStreamError.hasNoValue(); + + /// Set and emit the new value + set value(T newValue) => add(newValue); + + @override + Object get error => hasError + ? _wrapper!.errorAndStackTrace! + : throw ValueStreamError.hasNoError(); + + @override + Object? get errorOrNull => _wrapper?.errorAndStackTrace; + + @override + bool get hasError => _wrapper?.errorAndStackTrace != null; + + @override + StackTrace? get stackTrace => _wrapper?.errorAndStackTrace?.stackTrace; + + /// 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 { + final prefs = await SharedPreferences.getInstance(); + + 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 + 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) { + _wrapper = SubjectValueWrapper(value: val); + _controller.add(val); + } + + _onHydrate?.call(); + } + + 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', + ); + final errorAndTrace = ErrorAndStackTrace(error, StackTrace.current); + _wrapper = SubjectValueWrapper(errorAndStackTrace: errorAndTrace); + } + } + + @override + Subject createForwardingSubject({ + VoidCallback? onListen, + VoidCallback? onCancel, + bool sync = false, + HydrateCallback? hydrate, + PersistCallback? persist, + }) { + return HydratedSubject( + _key, + onListen: onListen, + onCancel: onCancel, + sync: sync, + hydrate: hydrate, + persist: persist, + ); + } +} diff --git a/lib/src/model/subject_value_wrapper.dart b/lib/src/model/subject_value_wrapper.dart new file mode 100644 index 0000000..2ffa359 --- /dev/null +++ b/lib/src/model/subject_value_wrapper.dart @@ -0,0 +1,11 @@ +import 'package:rxdart/rxdart.dart'; + +class SubjectValueWrapper { + final T? value; + final ErrorAndStackTrace? errorAndStackTrace; + + SubjectValueWrapper({ + this.value, + this.errorAndStackTrace, + }); +} diff --git a/lib/src/utils/typing_utils.dart b/lib/src/utils/typing_utils.dart new file mode 100644 index 0000000..df0c94c --- /dev/null +++ b/lib/src/utils/typing_utils.dart @@ -0,0 +1,5 @@ +class TypeUtils { + static bool areTypesEqual() { + return T1 == T2; + } +} diff --git a/pubspec.lock b/pubspec.lock index 0372fa6..15174fd 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -7,42 +7,63 @@ packages: name: async url: "https://pub.dartlang.org" source: hosted - version: "2.4.1" + version: "2.6.1" boolean_selector: dependency: transitive description: name: boolean_selector url: "https://pub.dartlang.org" source: hosted - version: "2.0.0" + version: "2.1.0" + characters: + dependency: transitive + description: + name: characters + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.0" charcode: dependency: transitive description: name: charcode url: "https://pub.dartlang.org" source: hosted - version: "1.1.3" + version: "1.2.0" clock: dependency: transitive description: name: clock url: "https://pub.dartlang.org" source: hosted - version: "1.0.1" + version: "1.1.0" collection: dependency: transitive description: name: collection url: "https://pub.dartlang.org" source: hosted - version: "1.14.12" + version: "1.15.0" fake_async: dependency: transitive description: name: fake_async url: "https://pub.dartlang.org" source: hosted - version: "1.1.0" + version: "1.2.0" + ffi: + dependency: transitive + description: + name: ffi + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.0" + file: + dependency: transitive + description: + name: file + url: "https://pub.dartlang.org" + source: hosted + version: "6.1.0" flutter: dependency: "direct main" description: flutter @@ -58,62 +79,125 @@ packages: description: flutter source: sdk version: "0.0.0" + js: + dependency: transitive + description: + name: js + url: "https://pub.dartlang.org" + source: hosted + version: "0.6.3" matcher: dependency: transitive description: name: matcher url: "https://pub.dartlang.org" source: hosted - version: "0.12.6" + version: "0.12.10" meta: dependency: transitive description: name: meta url: "https://pub.dartlang.org" source: hosted - version: "1.1.8" + version: "1.3.0" path: dependency: transitive description: name: path url: "https://pub.dartlang.org" source: hosted - version: "1.7.0" + version: "1.8.0" + path_provider_linux: + dependency: transitive + description: + name: path_provider_linux + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.0" + path_provider_platform_interface: + dependency: transitive + description: + name: path_provider_platform_interface + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.0" + path_provider_windows: + dependency: transitive + description: + name: path_provider_windows + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.0" + platform: + dependency: transitive + description: + name: platform + url: "https://pub.dartlang.org" + source: hosted + version: "3.0.0" + plugin_platform_interface: + dependency: transitive + description: + name: plugin_platform_interface + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.0" + process: + dependency: transitive + description: + name: process + url: "https://pub.dartlang.org" + source: hosted + version: "4.1.0" rxdart: dependency: "direct main" description: name: rxdart url: "https://pub.dartlang.org" source: hosted - version: "0.24.1" + version: "0.27.1" shared_preferences: dependency: "direct main" description: name: shared_preferences url: "https://pub.dartlang.org" source: hosted - version: "0.5.7+3" + version: "2.0.1" + shared_preferences_linux: + dependency: transitive + description: + name: shared_preferences_linux + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.0" shared_preferences_macos: dependency: transitive description: name: shared_preferences_macos url: "https://pub.dartlang.org" source: hosted - version: "0.0.1+3" + version: "2.0.0" shared_preferences_platform_interface: dependency: transitive description: name: shared_preferences_platform_interface url: "https://pub.dartlang.org" source: hosted - version: "1.0.1" + version: "2.0.0" shared_preferences_web: dependency: transitive description: name: shared_preferences_web url: "https://pub.dartlang.org" source: hosted - version: "0.1.2+2" + version: "2.0.0" + shared_preferences_windows: + dependency: transitive + description: + name: shared_preferences_windows + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.0" sky_engine: dependency: transitive description: flutter @@ -125,56 +209,70 @@ packages: name: source_span url: "https://pub.dartlang.org" source: hosted - version: "1.7.0" + version: "1.8.1" stack_trace: dependency: transitive description: name: stack_trace url: "https://pub.dartlang.org" source: hosted - version: "1.9.3" + version: "1.10.0" stream_channel: dependency: transitive description: name: stream_channel url: "https://pub.dartlang.org" source: hosted - version: "2.0.0" + version: "2.1.0" string_scanner: dependency: transitive description: name: string_scanner url: "https://pub.dartlang.org" source: hosted - version: "1.0.5" + version: "1.1.0" term_glyph: dependency: transitive description: name: term_glyph url: "https://pub.dartlang.org" source: hosted - version: "1.1.0" + version: "1.2.0" test_api: dependency: transitive description: name: test_api url: "https://pub.dartlang.org" source: hosted - version: "0.2.16" + version: "0.3.0" typed_data: dependency: transitive description: name: typed_data url: "https://pub.dartlang.org" source: hosted - version: "1.1.6" + version: "1.3.0" vector_math: dependency: transitive description: name: vector_math url: "https://pub.dartlang.org" source: hosted - version: "2.0.8" + version: "2.1.0" + win32: + dependency: transitive + description: + name: win32 + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.0" + xdg_directories: + dependency: transitive + description: + name: xdg_directories + url: "https://pub.dartlang.org" + source: hosted + version: "0.2.0" sdks: - dart: ">=2.7.0 <3.0.0" - flutter: ">=1.12.13+hotfix.5 <2.0.0" + dart: ">=2.12.0 <3.0.0" + flutter: ">=1.20.0" diff --git a/pubspec.yaml b/pubspec.yaml index 4ec9243..7587f08 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,17 +1,17 @@ name: hydrated description: An automatically persisted BehaviorSubject with simple hydration for Flutter. Intended to be used with the BLoC pattern. -version: 1.2.5+2 -homepage: https://github.com/lukepighetti/hydrated +version: 1.3.0-nullsafety.1 +homepage: https://github.com/solid-software/hydrated environment: - sdk: ">=2.6.0 <3.0.0" + sdk: '>=2.12.0 <3.0.0' dependencies: flutter: sdk: flutter - rxdart: ^0.24.1 - shared_preferences: ^0.5.7+3 + rxdart: ^0.27.0 + shared_preferences: ^2.0.1 dev_dependencies: flutter_test: diff --git a/test/hydrated_test.dart b/test/hydrated_test.dart index 3fe1e5b..074f145 100644 --- a/test/hydrated_test.dart +++ b/test/hydrated_test.dart @@ -1,11 +1,9 @@ import 'dart:async'; - -import 'package:flutter_test/flutter_test.dart'; - -import 'package:shared_preferences/shared_preferences.dart'; import 'dart:convert'; +import 'package:flutter_test/flutter_test.dart'; import 'package:hydrated/hydrated.dart'; +import 'package:shared_preferences/shared_preferences.dart'; void main() { SharedPreferences.setMockInitialValues({ @@ -22,7 +20,7 @@ void main() { final prefs = await SharedPreferences.getInstance(); final value = prefs.getBool("prefs"); - expect(value, equals(true)); + expect(value, isTrue); }); test('int', () async { @@ -58,16 +56,16 @@ void main() { final second = SerializedClass(false, 42); /// null before hydrate - expect(subject.value, equals(null)); + expect(subject.valueOrNull, isNull); /// properly hydrates await completer.future; - expect(subject.value.value, equals(true)); + expect(subject.value.value, isTrue); expect(subject.value.count, equals(42)); /// add values subject.add(second); - expect(subject.value.value, equals(false)); + expect(subject.value.value, isFalse); expect(subject.value.count, equals(42)); /// check value in store @@ -81,21 +79,23 @@ void main() { /// An example of a class that serializes to and from a string class SerializedClass { - bool value; - int count; + final bool value; + final int count; SerializedClass(this.value, this.count); - SerializedClass.fromJSON(String s) { + factory SerializedClass.fromJSON(String s) { final map = jsonDecode(s); - this.value = map['value']; - this.count = map['count']; + return SerializedClass( + map['value'], + map['count'], + ); } String toJSON() => jsonEncode({ - "value": this.value, - "count": this.count, + 'value': this.value, + 'count': this.count, }); } @@ -113,18 +113,18 @@ Future testHydrated( ); /// null before hydrate - expect(subject.value, equals(null)); - expect(subject.hasValue, equals(false)); + expect(subject.valueOrNull, isNull); + expect(subject.hasValue, isFalse); /// properly hydrates await completer.future; expect(subject.value, equals(first)); - expect(subject.hasValue, equals(true)); + expect(subject.hasValue, isTrue); /// add values subject.add(second); expect(subject.value, equals(second)); - expect(subject.hasValue, equals(true)); + expect(subject.hasValue, isTrue); /// check value in store final prefs = await SharedPreferences.getInstance();