diff --git a/lib/src/async_value_selector.dart b/lib/src/async_value_selector.dart deleted file mode 100644 index bd7e1a8..0000000 --- a/lib/src/async_value_selector.dart +++ /dev/null @@ -1,96 +0,0 @@ -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 Function? _set; - late final bool kActionWithArguments; - - 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(); - if (_set == null) return; - _requestQueue.add(() { - Function.apply(_set!, kActionWithArguments ? [newValue] : []); - }); - _processQueue(); - } - - /// Constructs an AsyncValueSelector with an initial value and a scope function. - AsyncValueSelector(this._value, this.scope, [this._set]) { - if (_set != null) { - var funcText = _set.runtimeType.toString(); - assert(!funcText.contains(','), 'Function must have one argument.'); - kActionWithArguments = !funcText.startsWith('() =>'); - } - } - - /// 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 index 5fb76c5..6c2c515 100644 --- a/lib/src/value_selector.dart +++ b/lib/src/value_selector.dart @@ -1,44 +1,63 @@ -part of '../value_selectable.dart'; +import 'package:flutter/foundation.dart'; /// A selector that computes a synchronous value based on a given scope. -class ValueSelector extends ValueSelectable { +class ValueSelector extends ChangeNotifier implements ValueListenable { final T Function(GetValue get) scope; - final Function? _set; - late final bool kActionWithArguments; late T _value; + final _listenables = {}; @override T get value => _value; - set value(dynamic newValue) { - if (_set != null) { - Function.apply(_set!, kActionWithArguments ? [newValue] : []); + late final _get = GetValue._(onListenableIdentified); + + void onListenableIdentified(Listenable listenable) { + if (_listenables.add(listenable)) { + listenable.addListener(notifyListeners); } } - late final _get = GetValue._(notifyListeners); - /// Constructs a ValueSelector with an initial value and a scope function. - ValueSelector(this.scope, [this._set]) { - if (_set != null) { - var funcText = _set.runtimeType.toString(); - assert(!funcText.contains(','), 'Function must have one argument.'); - kActionWithArguments = !funcText.startsWith('() =>'); - } - + ValueSelector(this.scope) { _value = scope(_get); - _get._tracking = false; } @override void notifyListeners() { - _value = scope(_get); + _listenables.clear(); + final newValue = scope(_get); + + if (newValue == _value) return; + _value = newValue; super.notifyListeners(); } @override void dispose() { _get.dispose(); + for (var listenable in _listenables) { + listenable.removeListener(notifyListeners); + } super.dispose(); } } + +/// Helper class to manage value dependencies and tracking for selectors. +final class GetValue { + final void Function(Listenable listenable) _selectorIdentifyListenable; + GetValue._(this._selectorIdentifyListenable); + + bool _isDisposed = false; + + /// Registers a notifier and returns its value. + R call(ValueListenable notifier) { + if (_isDisposed) throw Exception('It is disposed'); + _selectorIdentifyListenable.call(notifier); + return notifier.value; + } + + /// Disposes of all registered listeners. + void dispose() { + _isDisposed = true; + } +} diff --git a/lib/value_selectable.dart b/lib/value_selectable.dart index 17cc13c..2c83f11 100644 --- a/lib/value_selectable.dart +++ b/lib/value_selectable.dart @@ -1,12 +1,8 @@ library value_selectable; -import 'dart:async'; -import 'dart:collection'; - import 'package:flutter/foundation.dart'; -part 'src/async_value_selector.dart'; -part 'src/value_selector.dart'; +export 'src/value_selector.dart'; /// Abstract class for a selectable value, extending ChangeNotifier. abstract class ValueSelectable extends ChangeNotifier @@ -16,27 +12,20 @@ typedef SetValue = void Function(T value); /// Helper class to manage value dependencies and tracking for selectors. final class GetValue { - final void Function() _selectorNotifyListeners; - final List _disposers = []; - var _tracking = true; - bool _isDisposed = false; + final void Function(Listenable listenable) _selectorIdentifyListenable; + GetValue._(this._selectorIdentifyListenable); - GetValue._(this._selectorNotifyListeners); + bool _isDisposed = false; /// Registers a notifier and returns its value. R call(ValueListenable notifier) { - if (_tracking) { - notifier.addListener(_selectorNotifyListeners); - _disposers.add(() => notifier.removeListener(_selectorNotifyListeners)); - } + if (_isDisposed) throw Exception('It is disposed'); + _selectorIdentifyListenable.call(notifier); return notifier.value; } /// Disposes of all registered listeners. void dispose() { _isDisposed = true; - for (final disposer in _disposers) { - disposer(); - } } } diff --git a/test/src/async_value_selector_test.dart b/test/src/async_value_selector_test.dart deleted file mode 100644 index 8c67eef..0000000 --- a/test/src/async_value_selector_test.dart +++ /dev/null @@ -1,64 +0,0 @@ -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 index bf4a080..1e3d700 100644 --- a/test/src/value_selector_test.dart +++ b/test/src/value_selector_test.dart @@ -5,50 +5,80 @@ import 'package:value_selectable/value_selectable.dart'; void main() { test('ValueSelectable', () async { final counterState = ValueNotifier(0); - - final selectorState = ValueSelector( - (get) => get(counterState) + 1, - (String action) { - if (action == 'INCREMENT') counterState.value++; - if (action == 'DECREMENT') counterState.value--; - }, - ); + final selectorState = ValueSelector((get) => get(counterState) + 1); selectorState.addListener(expectAsync0(() { expect(selectorState.value, anyOf([2, 3])); - }, count: 3)); + }, count: 2)); // directly change counterState.value = 1; + counterState.value = 2; - // indirectly change - selectorState.value = 'INCREMENT'; - selectorState.value = 'DECREMENT'; - - await Future.delayed(const Duration(seconds: 1)); + // await Future.delayed(const Duration(seconds: 1)); selectorState.dispose(); }); - test('ValueSelectable throw assert if reducer has more one arguments', () async { - expect(() { - return ValueSelector( - (get) => 1, - (String action, int id) {}, - ); - }, throwsAssertionError); - }); - test('ValueSelectable action zero arguments', () async { - ValueSelector( - (get) => 1, - () {}, - ); + ValueSelector((get) => 1); }); test('ValueSelectable action one arguments', () async { - ValueSelector( - (get) => 1, - (String action) {}, - ); + final counterState = ValueNotifier(0); + ValueSelector((get) => get(counterState)); }); + + test('Listen other notifiers after the first value', () { + final mockCallback = Param1Callback(); + + final nameState = ValueNotifier('Deivão'); + final ageState = ValueNotifier(15); + + final canAccess = ValueSelector((get) { + if (get(nameState) == 'Deivão') return true; + if (get(ageState) >= 18) return true; + return false; + }); + + canAccess.addListener(() => mockCallback.call(canAccess.value)); + + // Return false + nameState.value = "Jacob"; + + // Return true (doesn't work) + ageState.value = 18; + + expect(mockCallback, orderedEquals([false, true])); + }); + + test('Do not emit repeated values', () { + final mockCallback = Param1Callback(); + + final nameState = ValueNotifier('Jacob'); + final ageState = ValueNotifier(17); + + final canAccess = ValueSelector((get) { + if (get(nameState) == 'Deivão') return true; + if (get(ageState) >= 18) return true; + return false; + }); + + canAccess.addListener(() => mockCallback.call(canAccess.value)); + + nameState.value = "Deivão"; + ageState.value = 17; + ageState.value = 16; + + expect(mockCallback, orderedEquals([true])); + }); +} + +class Param1Callback extends Iterable { + final _callStack = []; + void call(T value) { + _callStack.add(value); + } + + @override + Iterator get iterator => _callStack.iterator; }