From f29a602aced95b75b13fd18e38a536d4fe7eb1a8 Mon Sep 17 00:00:00 2001 From: Jacob Moura Date: Sun, 16 Jun 2024 12:18:08 -0300 Subject: [PATCH] added set --- .github/workflows/publish.yml | 25 ++++++ .gitignore | 2 + CHANGELOG.md | 4 + README.md | 29 +++++++ lib/src/async_value_selector.dart | 86 +++++++++++++++++++ lib/src/value_selector.dart | 33 ++++++++ lib/value_selectable.dart | 107 ++---------------------- pubspec.yaml | 2 +- test/src/async_value_selector_test.dart | 64 ++++++++++++++ test/src/value_selector_test.dart | 31 +++++++ test/value_selectable_test.dart | 57 ------------- 11 files changed, 283 insertions(+), 157 deletions(-) create mode 100644 .github/workflows/publish.yml create mode 100644 lib/src/async_value_selector.dart create mode 100644 lib/src/value_selector.dart create mode 100644 test/src/async_value_selector_test.dart create mode 100644 test/src/value_selector_test.dart delete mode 100644 test/value_selectable_test.dart diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 0000000..e3f3325 --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,25 @@ +# .github/workflows/publish.yml +name: Publish to pub.dev + +on: + push: + tags: + - 'v[0-9]+.[0-9]+.[0-9]+*' + +jobs: + publish: + permissions: + id-token: write + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - name: Set up Flutter + uses: subosito/flutter-action@v2 + with: + channel: stable + + - name: Install dependencies + run: flutter pub get + + - name: Publish + run: flutter pub publish --force \ No newline at end of file diff --git a/.gitignore b/.gitignore index 54d673a..f4f197b 100644 --- a/.gitignore +++ b/.gitignore @@ -37,3 +37,5 @@ app.*.symbols # Obfuscation related app.*.map.json +coverage/ + diff --git a/CHANGELOG.md b/CHANGELOG.md index f223231..6f9f166 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +# 1.1.0 + +- ADD: Set callback for ValueSelectable classes; + # 1.0.0+2 - First Version \ No newline at end of file diff --git a/README.md b/README.md index 505ced5..9023e77 100644 --- a/README.md +++ b/README.md @@ -77,6 +77,35 @@ void main() async { ``` +## Advance Usage + +As classes `ValueSelectable` trabalham devirando valores de outros `ValueListenable`, mas isso +também pode ser feito por meio de ações utilizando o setter do `value` de um `ValueSelectable`. +Note que ao alterar esse `value` nada acontecerá se a função `set` não for implementada no construtor de uma das classes do `ValueSelectable`. + +Com isso em mente poderemos fazer derivações utilizando ações de uma forma parecida com um `Reducer`: + +```dart + final counterState = ValueNotifier(0); + + final selectorState = ValueSelector( + (get) => get(counterState) + 1, + (action) { + if (action == 'INCREMENT') counterState.value++; + if (action == 'DECREMENT') counterState.value--; + }, + ); + + // directly change + counterState.value = 1; + + // indirectly change + selectorState.value = 'INCREMENT'; + selectorState.value = 'DECREMENT'; + +``` + + ## Contributing Contributions are welcome! Please open an issue or submit a pull request. \ No newline at end of file diff --git a/lib/src/async_value_selector.dart b/lib/src/async_value_selector.dart new file mode 100644 index 0000000..3cca279 --- /dev/null +++ b/lib/src/async_value_selector.dart @@ -0,0 +1,86 @@ +part of '../value_selectable.dart'; + +/// A selector that computes an asynchronous value based on a given scope. +class AsyncValueSelector extends ValueSelectable { + final FutureOr Function(GetValue get) scope; + final FutureOr Function(dynamic action)? _set; + + final Queue _requestQueue = Queue(); + bool _isProcessing = false; + bool _isInitialized = false; + + late final _get = GetValue._(notifyListeners); + final _readyCompleter = Completer(); + + /// Future that completes when the selector is ready. + Future get isReady { + _initialize(); + return _readyCompleter.future; + } + + late T _value; + + void _initialize() { + if (!_isInitialized) { + _isInitialized = true; + _requestQueue.add(_initializeSelector); + _processQueue(); + } + } + + @override + T get value { + _initialize(); + return _value; + } + + set value(dynamic newValue) { + _initialize(); + _requestQueue.add(() => _set?.call(newValue)); + _processQueue(); + } + + /// Constructs an AsyncValueSelector with an initial value and a scope function. + AsyncValueSelector(this._value, this.scope, [this._set]); + + /// Processes the request queue, ensuring only one request is processed at a time. + Future _processQueue() async { + if (_isProcessing || _requestQueue.isEmpty) return; + + _isProcessing = true; + try { + while (_requestQueue.isNotEmpty) { + final request = _requestQueue.removeFirst(); + await request(); + } + } catch (e) { + rethrow; + } finally { + _isProcessing = false; + } + } + + @override + void notifyListeners() { + _requestQueue.add(() async { + _value = await scope(_get); + if (_get._isDisposed) return; + super.notifyListeners(); + }); + _processQueue(); + } + + /// Initializes the selector by computing the initial value. + Future _initializeSelector() async { + _value = await scope(_get); + super.notifyListeners(); + _get._tracking = false; + _readyCompleter.complete(true); + } + + @override + void dispose() { + _get.dispose(); + super.dispose(); + } +} diff --git a/lib/src/value_selector.dart b/lib/src/value_selector.dart new file mode 100644 index 0000000..bb5d3bb --- /dev/null +++ b/lib/src/value_selector.dart @@ -0,0 +1,33 @@ +part of '../value_selectable.dart'; + +/// A selector that computes a synchronous value based on a given scope. +class ValueSelector extends ValueSelectable { + final T Function(GetValue get) scope; + final FutureOr Function(dynamic action)? _set; + late T _value; + + @override + T get value => _value; + + set value(dynamic newValue) => _set?.call(newValue); + + late final _get = GetValue._(notifyListeners); + + /// Constructs a ValueSelector with an initial value and a scope function. + ValueSelector(this.scope, [this._set]) { + _value = scope(_get); + _get._tracking = false; + } + + @override + void notifyListeners() { + _value = scope(_get); + super.notifyListeners(); + } + + @override + void dispose() { + _get.dispose(); + super.dispose(); + } +} diff --git a/lib/value_selectable.dart b/lib/value_selectable.dart index df43e78..17cc13c 100644 --- a/lib/value_selectable.dart +++ b/lib/value_selectable.dart @@ -5,115 +5,23 @@ import 'dart:collection'; import 'package:flutter/foundation.dart'; +part 'src/async_value_selector.dart'; +part 'src/value_selector.dart'; + /// Abstract class for a selectable value, extending ChangeNotifier. abstract class ValueSelectable extends ChangeNotifier implements ValueListenable {} -/// A selector that computes a synchronous value based on a given scope. -class ValueSelector extends ValueSelectable { - final T Function(ValueRegistrator get) scope; - late T _value; - - @override - T get value => _value; - - late final _get = ValueRegistrator._(notifyListeners); - - /// Constructs a ValueSelector with an initial value and a scope function. - ValueSelector(this.scope) { - _value = scope(_get); - _get._tracking = false; - } - - @override - void notifyListeners() { - _value = scope(_get); - super.notifyListeners(); - } - - @override - void dispose() { - _get.dispose(); - super.dispose(); - } -} - -/// A selector that computes an asynchronous value based on a given scope. -class AsyncValueSelector extends ValueSelectable { - final FutureOr Function(ValueRegistrator get) scope; - final Queue _requestQueue = Queue(); - bool _isProcessing = false; - - late final _get = ValueRegistrator._(notifyListeners); - final _readyCompleter = Completer(); - - /// Future that completes when the selector is ready. - Future get isReady => _readyCompleter.future; - - late T _value; - - @override - T get value => _value; - - /// Constructs an AsyncValueSelector with an initial value and a scope function. - AsyncValueSelector(this._value, this.scope) { - _requestQueue.add(_initializeSelector); - _processQueue(); - } - - /// Processes the request queue, ensuring only one request is processed at a time. - Future _processQueue() async { - if (_isProcessing || _requestQueue.isEmpty) return; - - _isProcessing = true; - try { - while (_requestQueue.isNotEmpty) { - final request = _requestQueue.removeFirst(); - await request(); - } - } catch (e) { - rethrow; - } finally { - _isProcessing = false; - } - } - - @override - void notifyListeners() { - _requestQueue.add(() async { - _value = await scope(_get); - super.notifyListeners(); - }); - _processQueue(); - } - - /// Initializes the selector by computing the initial value. - Future _initializeSelector() async { - try { - _value = await scope(_get); - notifyListeners(); - _get._tracking = false; - _readyCompleter.complete(true); - } catch (e) { - _readyCompleter.completeError(e); - rethrow; - } - } - - @override - void dispose() { - _get.dispose(); - super.dispose(); - } -} +typedef SetValue = void Function(T value); /// Helper class to manage value dependencies and tracking for selectors. -final class ValueRegistrator { +final class GetValue { final void Function() _selectorNotifyListeners; final List _disposers = []; var _tracking = true; + bool _isDisposed = false; - ValueRegistrator._(this._selectorNotifyListeners); + GetValue._(this._selectorNotifyListeners); /// Registers a notifier and returns its value. R call(ValueListenable notifier) { @@ -126,6 +34,7 @@ final class ValueRegistrator { /// Disposes of all registered listeners. void dispose() { + _isDisposed = true; for (final disposer in _disposers) { disposer(); } diff --git a/pubspec.yaml b/pubspec.yaml index 7a4c558..b300acd 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -2,7 +2,7 @@ name: value_selectable description: "A Flutter package that provides computed values for `ValueNotifier`, inspired by the Selectors from Recoil." repository: https://github.com/Flutterando/value_selectable -version: 1.0.0+2 +version: 1.1.0 environment: sdk: '>=3.0.0 <4.0.0' diff --git a/test/src/async_value_selector_test.dart b/test/src/async_value_selector_test.dart new file mode 100644 index 0000000..8c67eef --- /dev/null +++ b/test/src/async_value_selector_test.dart @@ -0,0 +1,64 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:value_selectable/value_selectable.dart'; + +void main() { + test('should compute initial value correctly', () async { + final valueNotifier = ValueNotifier(1); + final selector = AsyncValueSelector( + 0, + (get) async => get(valueNotifier) + 1, + (action) => valueNotifier.value = action as int, + ); + + selector.addListener(expectAsync0(() { + expect(selector.value, anyOf([2, 4])); + }, count: 2)); + + selector.value = 3; + }); + + test('should update value when dependent notifiers change', () async { + final valueNotifier = ValueNotifier(1); + final selector = + AsyncValueSelector(0, (get) async => get(valueNotifier) + 1); + + await selector.isReady; + valueNotifier.value = 2; + await Future.delayed( + Duration.zero); // Allow notifyListeners to be processed + expect(selector.value, 3); + }); + + test('should notify listeners on value change', () async { + final valueNotifier = ValueNotifier(1); + final selector = + AsyncValueSelector(0, (get) async => get(valueNotifier) + 1); + + int listenerCallCount = 0; + selector.addListener(() { + listenerCallCount++; + }); + + await selector.isReady; + + valueNotifier.value = 2; + await Future.delayed( + Duration.zero); // Allow notifyListeners to be processed + expect(listenerCallCount, 2); + expect(selector.value, 3); + }); + + test('should dispose correctly', () async { + final valueNotifier = ValueNotifier(1); + final selector = + AsyncValueSelector(0, (get) async => get(valueNotifier) + 1); + + await selector.isReady; + + selector.dispose(); + + valueNotifier.value = 2; + // Ensure no exceptions are thrown on dispose + }); +} diff --git a/test/src/value_selector_test.dart b/test/src/value_selector_test.dart new file mode 100644 index 0000000..23a4a8d --- /dev/null +++ b/test/src/value_selector_test.dart @@ -0,0 +1,31 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:value_selectable/value_selectable.dart'; + +void main() { + test('ValueSelectable', () async { + final counterState = ValueNotifier(0); + + final selectorState = ValueSelector( + (get) => get(counterState) + 1, + (action) { + if (action == 'INCREMENT') counterState.value++; + if (action == 'DECREMENT') counterState.value--; + }, + ); + + selectorState.addListener(expectAsync0(() { + expect(selectorState.value, anyOf([2, 3])); + }, count: 3)); + + // directly change + counterState.value = 1; + + // indirectly change + selectorState.value = 'INCREMENT'; + selectorState.value = 'DECREMENT'; + + await Future.delayed(const Duration(seconds: 1)); + selectorState.dispose(); + }); +} diff --git a/test/value_selectable_test.dart b/test/value_selectable_test.dart deleted file mode 100644 index 3496c50..0000000 --- a/test/value_selectable_test.dart +++ /dev/null @@ -1,57 +0,0 @@ -import 'package:flutter/widgets.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:value_selectable/value_selectable.dart'; - -void main() { - group('AsyncValueSelector', () { - test('should compute initial value correctly', () async { - final valueNotifier = ValueNotifier(1); - final selector = - AsyncValueSelector(0, (get) async => get(valueNotifier) + 1); - - await selector.isReady; - expect(selector.value, 2); - }); - - test('should update value when dependent notifiers change', () async { - final valueNotifier = ValueNotifier(1); - final selector = - AsyncValueSelector(0, (get) async => get(valueNotifier) + 1); - - await selector.isReady; - valueNotifier.value = 2; - await Future.delayed( - Duration.zero); // Allow notifyListeners to be processed - expect(selector.value, 3); - }); - - test('should notify listeners on value change', () async { - final valueNotifier = ValueNotifier(1); - final selector = - AsyncValueSelector(0, (get) async => get(valueNotifier) + 1); - - await selector.isReady; - - int listenerCallCount = 0; - selector.addListener(() { - listenerCallCount++; - }); - - valueNotifier.value = 2; - await Future.delayed( - Duration.zero); // Allow notifyListeners to be processed - expect(listenerCallCount, 1); - expect(selector.value, 3); - }); - - test('should dispose correctly', () async { - final valueNotifier = ValueNotifier(1); - final selector = - AsyncValueSelector(0, (get) async => get(valueNotifier) + 1); - - await selector.isReady; - selector.dispose(); - // Ensure no exceptions are thrown on dispose - }); - }); -}