diff --git a/packages/ui_primitives/lib/src/change_notifier.dart b/packages/ui_primitives/lib/src/change_notifier.dart new file mode 100644 index 000000000..71ccfb0e7 --- /dev/null +++ b/packages/ui_primitives/lib/src/change_notifier.dart @@ -0,0 +1,404 @@ +// Copyright 2025 The Flutter Authors. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// ignore_for_file: comment_references, lines_longer_than_80_chars + +/// @docImport 'package:flutter/animation.dart'; +/// @docImport 'package:flutter/widgets.dart'; +library; + +import 'package:meta/meta.dart'; + +import 'error_reporter.dart'; +import 'interfaces.dart'; +import 'primitives.dart'; +import 'private_leak_tracking.dart'; + +/// A class that can be extended or mixed in that provides a change notification +/// API using [VoidCallback] for notifications. +/// +/// It is O(1) for adding listeners and O(N) for removing listeners and dispatching +/// notifications (where N is the number of listeners). +/// +/// ## Using ChangeNotifier subclasses for data models +/// +/// A data structure can extend or mix in [ChangeNotifier] to implement the +/// [Listenable] interface and thus become usable with widgets that listen for +/// changes to [Listenable]s, such as [ListenableBuilder]. +/// +/// {@tool dartpad} +/// The following example implements a simple counter that utilizes a +/// [ListenableBuilder] to limit rebuilds to only the [Text] widget containing +/// the count. The current count is stored in a [ChangeNotifier] subclass, which +/// rebuilds the [ListenableBuilder]'s contents when its value is changed. +/// +/// ** See code in examples/api/lib/widgets/transitions/listenable_builder.2.dart ** +/// {@end-tool} +/// +/// {@tool dartpad} +/// In this case, the [ChangeNotifier] subclass encapsulates a list, and notifies +/// the clients any time an item is added to the list. This example only supports +/// adding items; as an exercise, consider adding buttons to remove items from +/// the list as well. +/// +/// ** See code in examples/api/lib/widgets/transitions/listenable_builder.3.dart ** +/// {@end-tool} +/// +/// See also: +/// +/// * [ValueNotifier], which is a [ChangeNotifier] that wraps a single value. +mixin class ChangeNotifier implements Listenable { + int _count = 0; + // The _listeners is intentionally set to a fixed-length _GrowableList instead + // of const []. + // + // The const [] creates an instance of _ImmutableList which would be + // different from fixed-length _GrowableList used elsewhere in this class. + // keeping runtime type the same during the lifetime of this class lets the + // compiler to infer concrete type for this property, and thus improves + // performance. + static final List _emptyListeners = List.filled( + 0, + null, + ); + List _listeners = _emptyListeners; + int _notificationCallStackDepth = 0; + int _reentrantlyRemovedListeners = 0; + bool _debugDisposed = false; + + /// If true, the event [ObjectCreated] for this instance was dispatched to + /// [FlutterMemoryAllocations]. + /// + /// As [ChangeNotifier] is used as mixin, it does not have constructor, + /// so we use [addListener] to dispatch the event. + bool _debugCreationDispatched = false; + + /// Used by subclasses to assert that the [ChangeNotifier] has not yet been + /// disposed. + /// + /// {@tool snippet} + /// The [debugAssertNotDisposed] function should only be called inside of an + /// assert, as in this example. + /// + /// ```dart + /// class MyNotifier with ChangeNotifier { + /// void doUpdate() { + /// assert(ChangeNotifier.debugAssertNotDisposed(this)); + /// // ... + /// } + /// } + /// ``` + /// {@end-tool} + // This is static and not an instance method because too many people try to + // implement ChangeNotifier instead of extending it (and so it is too breaking + // to add a method, especially for debug). + static bool debugAssertNotDisposed(ChangeNotifier notifier) { + assert(() { + if (notifier._debugDisposed) { + throw FrameworkErrorReporter.instance.createError( + 'A ${notifier.runtimeType} was used after being disposed.\n' + 'Once you have called dispose() on a ${notifier.runtimeType}, it ' + 'can no longer be used.', + ); + } + return true; + }()); + return true; + } + + /// Whether any listeners are currently registered. + /// + /// Clients should not depend on this value for their behavior, because having + /// one listener's logic change when another listener happens to start or stop + /// listening will lead to extremely hard-to-track bugs. Subclasses might use + /// this information to determine whether to do any work when there are no + /// listeners, however; for example, resuming a [Stream] when a listener is + /// added and pausing it when a listener is removed. + /// + /// Typically this is used by overriding [addListener], checking if + /// [hasListeners] is false before calling `super.addListener()`, and if so, + /// starting whatever work is needed to determine when to call + /// [notifyListeners]; and similarly, by overriding [removeListener], checking + /// if [hasListeners] is false after calling `super.removeListener()`, and if + /// so, stopping that same work. + /// + /// This method returns false if [dispose] has been called. + @protected + bool get hasListeners => _count > 0; + + /// Dispatches event of the [object] creation to [FlutterMemoryAllocations.instance]. + /// + /// If the event was already dispatched or [kFlutterMemoryAllocationsEnabled] + /// is false, the method is noop. + /// + /// Tools like leak_tracker use the event of object creation to help + /// developers identify the owner of the object, for troubleshooting purposes, + /// by taking stack trace at the moment of the event. + /// + /// But, as [ChangeNotifier] is mixin, it does not have its own constructor. So, it + /// communicates object creation in first `addListener`, that results + /// in the stack trace pointing to `addListener`, not to constructor. + /// + /// To make debugging easier, invoke [ChangeNotifier.maybeDispatchObjectCreation] + /// in constructor of the class. It will help + /// to identify the owner. + /// + /// Make sure to invoke it with condition `if (kFlutterMemoryAllocationsEnabled) ...` + /// so that the method is tree-shaken away when the flag is false. + @protected + static void maybeDispatchObjectCreation(ChangeNotifier object) { + assert(() { + if (object._debugCreationDispatched) { + return true; + } + object._debugCreationDispatched = true; + if (kTrackMemoryLeaks) { + debugMaybeDispatchCreated(object.runtimeType.toString(), object); + } + return true; + }()); + } + + /// Register a closure to be called when the object changes. + /// + /// If the given closure is already registered, an additional instance is + /// added, and must be removed the same number of times it is added before it + /// will stop being called. + /// + /// This method must not be called after [dispose] has been called. + /// + /// {@template flutter.foundation.ChangeNotifier.addListener} + /// If a listener is added twice, and is removed once during an iteration + /// (e.g. in response to a notification), it will still be called again. If, + /// on the other hand, it is removed as many times as it was registered, then + /// it will no longer be called. This odd behavior is the result of the + /// [ChangeNotifier] not being able to determine which listener is being + /// removed, since they are identical, therefore it will conservatively still + /// call all the listeners when it knows that any are still registered. + /// + /// This surprising behavior can be unexpectedly observed when registering a + /// listener on two separate objects which are both forwarding all + /// registrations to a common upstream object. + /// {@endtemplate} + /// + /// See also: + /// + /// * [removeListener], which removes a previously registered closure from + /// the list of closures that are notified when the object changes. + @override + void addListener(VoidCallback listener) { + assert(ChangeNotifier.debugAssertNotDisposed(this)); + + maybeDispatchObjectCreation(this); + + if (_count == _listeners.length) { + if (_count == 0) { + _listeners = List.filled(1, null); + } else { + final newListeners = List.filled( + _listeners.length * 2, + null, + ); + for (var i = 0; i < _count; i++) { + newListeners[i] = _listeners[i]; + } + _listeners = newListeners; + } + } + _listeners[_count++] = listener; + } + + void _removeAt(int index) { + // The list holding the listeners is not growable for performances reasons. + // We still want to shrink this list if a lot of listeners have been added + // and then removed outside a notifyListeners iteration. + // We do this only when the real number of listeners is half the length + // of our list. + _count -= 1; + if (_count * 2 <= _listeners.length) { + final newListeners = List.filled(_count, null); + + // Listeners before the index are at the same place. + for (var i = 0; i < index; i++) { + newListeners[i] = _listeners[i]; + } + + // Listeners after the index move towards the start of the list. + for (var i = index; i < _count; i++) { + newListeners[i] = _listeners[i + 1]; + } + + _listeners = newListeners; + } else { + // When there are more listeners than half the length of the list, we only + // shift our listeners, so that we avoid to reallocate memory for the + // whole list. + for (var i = index; i < _count; i++) { + _listeners[i] = _listeners[i + 1]; + } + _listeners[_count] = null; + } + } + + /// Remove a previously registered closure from the list of closures that are + /// notified when the object changes. + /// + /// If the given listener is not registered, the call is ignored. + /// + /// This method returns immediately if [dispose] has been called. + /// + /// {@macro flutter.foundation.ChangeNotifier.addListener} + /// + /// See also: + /// + /// * [addListener], which registers a closure to be called when the object + /// changes. + @override + void removeListener(VoidCallback listener) { + // This method is allowed to be called on disposed instances for usability + // reasons. Due to how our frame scheduling logic between render objects and + // overlays, it is common that the owner of this instance would be disposed a + // frame earlier than the listeners. Allowing calls to this method after it + // is disposed makes it easier for listeners to properly clean up. + for (var i = 0; i < _count; i++) { + final VoidCallback? listenerAtIndex = _listeners[i]; + if (listenerAtIndex == listener) { + if (_notificationCallStackDepth > 0) { + // We don't resize the list during notifyListeners iterations + // but we set to null, the listeners we want to remove. We will + // effectively resize the list at the end of all notifyListeners + // iterations. + _listeners[i] = null; + _reentrantlyRemovedListeners++; + } else { + // When we are outside the notifyListeners iterations we can + // effectively shrink the list. + _removeAt(i); + } + break; + } + } + } + + /// Discards any resources used by the object. + /// + /// After this is called, the object is not in a usable state and should be + /// discarded (calls to [addListener] will throw after the object is disposed). + /// + /// This method should only be called by the object's owner. + /// + /// This method does not notify listeners, and clears the listener list once + /// it is called. Consumers of this class must decide on whether to notify + /// listeners or not immediately before disposal. + @mustCallSuper + void dispose() { + assert(ChangeNotifier.debugAssertNotDisposed(this)); + assert( + _notificationCallStackDepth == 0, + 'The "dispose()" method on $this was called during the call to ' + '"notifyListeners()". This is likely to cause errors since it modifies ' + 'the list of listeners while the list is being used.', + ); + assert(() { + _debugDisposed = true; + if (kTrackMemoryLeaks && _debugCreationDispatched) { + debugMaybeDispatchDisposed(this); + } + return true; + }()); + _listeners = _emptyListeners; + _count = 0; + } + + /// Call all the registered listeners. + /// + /// Call this method whenever the object changes, to notify any clients the + /// object may have changed. Listeners that are added during this iteration + /// will not be visited. Listeners that are removed during this iteration will + /// not be visited after they are removed. + /// + /// Exceptions thrown by listeners will be caught and reported using + /// [FlutterError.reportError]. + /// + /// This method must not be called after [dispose] has been called. + /// + /// Surprising behavior can result when reentrantly removing a listener (e.g. + /// in response to a notification) that has been registered multiple times. + /// See the discussion at [removeListener]. + @protected + @visibleForTesting + @pragma('vm:notify-debugger-on-exception') + void notifyListeners() { + assert(ChangeNotifier.debugAssertNotDisposed(this)); + if (_count == 0) { + return; + } + + // To make sure that listeners removed during this iteration are not called, + // we set them to null, but we don't shrink the list right away. + // By doing this, we can continue to iterate on our list until it reaches + // the last listener added before the call to this method. + + // To allow potential listeners to recursively call notifyListener, we track + // the number of times this method is called in _notificationCallStackDepth. + // Once every recursive iteration is finished (i.e. when _notificationCallStackDepth == 0), + // we can safely shrink our list so that it will only contain not null + // listeners. + + _notificationCallStackDepth++; + + final int end = _count; + for (var i = 0; i < end; i++) { + try { + _listeners[i]?.call(); + } catch (exception, stack) { + FrameworkErrorReporter.instance.report( + FrameworkErrorDetails( + exception: exception, + stack: stack, + dispatchingObject: runtimeType, + ), + ); + } + } + + _notificationCallStackDepth--; + + if (_notificationCallStackDepth == 0 && _reentrantlyRemovedListeners > 0) { + // We really remove the listeners when all notifications are done. + final int newLength = _count - _reentrantlyRemovedListeners; + if (newLength * 2 <= _listeners.length) { + // As in _removeAt, we only shrink the list when the real number of + // listeners is half the length of our list. + final newListeners = List.filled(newLength, null); + + var newIndex = 0; + for (var i = 0; i < _count; i++) { + final VoidCallback? listener = _listeners[i]; + if (listener != null) { + newListeners[newIndex++] = listener; + } + } + + _listeners = newListeners; + } else { + // Otherwise we put all the null references at the end. + for (var i = 0; i < newLength; i += 1) { + if (_listeners[i] == null) { + // We swap this item with the next not null item. + int swapIndex = i + 1; + while (_listeners[swapIndex] == null) { + swapIndex += 1; + } + _listeners[i] = _listeners[swapIndex]; + _listeners[swapIndex] = null; + } + } + } + + _reentrantlyRemovedListeners = 0; + _count = newLength; + } + } +} diff --git a/packages/ui_primitives/lib/src/error_reporter.dart b/packages/ui_primitives/lib/src/error_reporter.dart index 668e42de1..5341c6ab0 100644 --- a/packages/ui_primitives/lib/src/error_reporter.dart +++ b/packages/ui_primitives/lib/src/error_reporter.dart @@ -7,26 +7,24 @@ class FrameworkErrorReporter { static FrameworkErrorReporter instance = FrameworkErrorReporter(); /// Creates a the framework specific error with the given message. - Error createError(String message, {FrameworkErrorDetails? details}) => - _FrameworkError(message: message, details: details); + Error createError(String message) => + _FrameworkError(FrameworkErrorDetails(exception: message)); /// Reports [FrameworkErrorDetails] according to the framework settings. /// /// Depending on settings, it may throw an exception, log an error, /// or debug-stop the execution. - void report(FrameworkErrorDetails details) => - throw _FrameworkError(details: details); + void report(FrameworkErrorDetails details) => throw _FrameworkError(details); } final class _FrameworkError extends Error { - _FrameworkError({this.message, this.details}); + _FrameworkError(this.details); - final FrameworkErrorDetails? details; - final String? message; + final FrameworkErrorDetails details; @override String toString() { - return [message, details].where((e) => e != null).join('\n'); + return details.toString(); } } diff --git a/packages/ui_primitives/lib/src/listenable.dart b/packages/ui_primitives/lib/src/interfaces.dart similarity index 100% rename from packages/ui_primitives/lib/src/listenable.dart rename to packages/ui_primitives/lib/src/interfaces.dart diff --git a/packages/ui_primitives/lib/src/private_leak_tracking.dart b/packages/ui_primitives/lib/src/private_leak_tracking.dart index 9db9b8a3b..66e6cba1a 100644 --- a/packages/ui_primitives/lib/src/private_leak_tracking.dart +++ b/packages/ui_primitives/lib/src/private_leak_tracking.dart @@ -5,9 +5,10 @@ import 'package:leak_tracker/leak_tracker.dart'; // TODO(polinach): move this constant to leak_tracker package. -const bool kTrackMemoryLeaks = bool.fromEnvironment( - 'leak_tracker.track_memory_leaks', -); +const bool kTrackMemoryLeaks = true; +// bool.fromEnvironment( +// 'leak_tracker.track_memory_leaks', +// ); /// The name of this library. /// diff --git a/packages/ui_primitives/lib/src/value_notifier.dart b/packages/ui_primitives/lib/src/value_notifier.dart index 71e48544a..2d1b3e36b 100644 --- a/packages/ui_primitives/lib/src/value_notifier.dart +++ b/packages/ui_primitives/lib/src/value_notifier.dart @@ -6,366 +6,13 @@ /// @docImport 'package:flutter/widgets.dart'; library; -import 'package:meta/meta.dart'; - +import 'change_notifier.dart'; import 'error_reporter.dart'; -import 'listenable.dart'; +import 'interfaces.dart'; import 'primitives.dart'; import 'private_leak_tracking.dart'; -/// A class that can be extended or mixed in that provides a change notification -/// API using [VoidCallback] for notifications. -/// -/// It is O(1) for adding listeners and O(N) for removing listeners and -/// dispatching -/// notifications (where N is the number of listeners). -/// -/// ## Using ChangeNotifier subclasses for data models -/// -/// A data structure can extend or mix in [_ChangeNotifier] to implement the -/// [Listenable] interface and thus become usable with widgets that listen for -/// changes to [Listenable]s, such as [ListenableBuilder]. -/// -/// {@tool dartpad} -/// The following example implements a simple counter that utilizes a -/// [ListenableBuilder] to limit rebuilds to only the [Text] widget containing -/// the count. The current count is stored in a [_ChangeNotifier] subclass, -/// which -/// rebuilds the [ListenableBuilder]'s contents when its value is changed. -/// -/// ** See code in -/// examples/api/lib/widgets/transitions/listenable_builder.2.dart ** -/// {@end-tool} -/// -/// {@tool dartpad} -/// In this case, the [_ChangeNotifier] subclass encapsulates a list, and -/// notifies -/// the clients any time an item is added to the list. This example only -/// supports -/// adding items; as an exercise, consider adding buttons to remove items from -/// the list as well. -/// -/// ** See code in -/// examples/api/lib/widgets/transitions/listenable_builder.3.dart ** -/// {@end-tool} -/// -/// See also: -/// -/// * [ValueNotifier], which is a [_ChangeNotifier] that wraps a single value. -class _ChangeNotifier implements Listenable { - int _count = 0; - // The _listeners is intentionally set to a fixed-length _GrowableList instead - // of const []. - // - // The const [] creates an instance of _ImmutableList which would be - // different from fixed-length _GrowableList used elsewhere in this class. - // keeping runtime type the same during the lifetime of this class lets the - // compiler to infer concrete type for this property, and thus improves - // performance. - static final List _emptyListeners = List.filled( - 0, - null, - ); - List _listeners = _emptyListeners; - int _notificationCallStackDepth = 0; - int _reentrantlyRemovedListeners = 0; - bool _debugDisposed = false; - - /// Used by subclasses to assert that the [_ChangeNotifier] has not yet been - /// disposed. - /// - /// {@tool snippet} - /// The [debugAssertNotDisposed] function should only be called inside of an - /// assert, as in this example. - /// - /// ```dart - /// class MyNotifier with ChangeNotifier { - /// void doUpdate() { - /// assert(ChangeNotifier.debugAssertNotDisposed(this)); - /// // ... - /// } - /// } - /// ``` - /// {@end-tool} - // This is static and not an instance method because too many people try to - // implement ChangeNotifier instead of extending it (and so it is too breaking - // to add a method, especially for debug). - static bool debugAssertNotDisposed(_ChangeNotifier notifier) { - assert(() { - if (notifier._debugDisposed) { - throw FrameworkErrorReporter.instance.createError( - 'A ${notifier.runtimeType} was used after being disposed.\n' - 'Once you have called dispose() on a ${notifier.runtimeType}, it ' - 'can no longer be used.', - ); - } - return true; - }()); - return true; - } - - /// Whether any listeners are currently registered. - /// - /// Clients should not depend on this value for their behavior, because having - /// one listener's logic change when another listener happens to start or stop - /// listening will lead to extremely hard-to-track bugs. Subclasses might use - /// this information to determine whether to do any work when there are no - /// listeners, however; for example, resuming a [Stream] when a listener is - /// added and pausing it when a listener is removed. - /// - /// Typically this is used by overriding [addListener], checking if - /// [hasListeners] is false before calling `super.addListener()`, and if so, - /// starting whatever work is needed to determine when to call - /// [notifyListeners]; and similarly, by overriding [removeListener], checking - /// if [hasListeners] is false after calling `super.removeListener()`, and if - /// so, stopping that same work. - /// - /// This method returns false if [dispose] has been called. - @protected - bool get hasListeners => _count > 0; - - /// Register a closure to be called when the object changes. - /// - /// If the given closure is already registered, an additional instance is - /// added, and must be removed the same number of times it is added before it - /// will stop being called. - /// - /// This method must not be called after [dispose] has been called. - /// - /// {@template flutter.foundation.ChangeNotifier.addListener} - /// If a listener is added twice, and is removed once during an iteration - /// (e.g. in response to a notification), it will still be called again. If, - /// on the other hand, it is removed as many times as it was registered, then - /// it will no longer be called. This odd behavior is the result of the - /// [_ChangeNotifier] not being able to determine which listener is being - /// removed, since they are identical, therefore it will conservatively still - /// call all the listeners when it knows that any are still registered. - /// - /// This surprising behavior can be unexpectedly observed when registering a - /// listener on two separate objects which are both forwarding all - /// registrations to a common upstream object. - /// {@endtemplate} - /// - /// See also: - /// - /// * [removeListener], which removes a previously registered closure from - /// the list of closures that are notified when the object changes. - @override - void addListener(VoidCallback listener) { - assert(_ChangeNotifier.debugAssertNotDisposed(this)); - - if (_count == _listeners.length) { - if (_count == 0) { - _listeners = List.filled(1, null); - } else { - final newListeners = List.filled( - _listeners.length * 2, - null, - ); - for (var i = 0; i < _count; i++) { - newListeners[i] = _listeners[i]; - } - _listeners = newListeners; - } - } - _listeners[_count++] = listener; - } - - void _removeAt(int index) { - // The list holding the listeners is not growable for performances reasons. - // We still want to shrink this list if a lot of listeners have been added - // and then removed outside a notifyListeners iteration. - // We do this only when the real number of listeners is half the length - // of our list. - _count -= 1; - if (_count * 2 <= _listeners.length) { - final newListeners = List.filled(_count, null); - - // Listeners before the index are at the same place. - for (var i = 0; i < index; i++) { - newListeners[i] = _listeners[i]; - } - - // Listeners after the index move towards the start of the list. - for (var i = index; i < _count; i++) { - newListeners[i] = _listeners[i + 1]; - } - - _listeners = newListeners; - } else { - // When there are more listeners than half the length of the list, we only - // shift our listeners, so that we avoid to reallocate memory for the - // whole list. - for (var i = index; i < _count; i++) { - _listeners[i] = _listeners[i + 1]; - } - _listeners[_count] = null; - } - } - - /// Remove a previously registered closure from the list of closures that are - /// notified when the object changes. - /// - /// If the given listener is not registered, the call is ignored. - /// - /// This method returns immediately if [dispose] has been called. - /// - /// {@macro flutter.foundation.ChangeNotifier.addListener} - /// - /// See also: - /// - /// * [addListener], which registers a closure to be called when the object - /// changes. - @override - void removeListener(VoidCallback listener) { - // This method is allowed to be called on disposed instances for usability - // reasons. Due to how our frame scheduling logic between render objects and - // overlays, it is common that the owner of this instance would be disposed - // a - // frame earlier than the listeners. Allowing calls to this method after it - // is disposed makes it easier for listeners to properly clean up. - for (var i = 0; i < _count; i++) { - final VoidCallback? listenerAtIndex = _listeners[i]; - if (listenerAtIndex == listener) { - if (_notificationCallStackDepth > 0) { - // We don't resize the list during notifyListeners iterations - // but we set to null, the listeners we want to remove. We will - // effectively resize the list at the end of all notifyListeners - // iterations. - _listeners[i] = null; - _reentrantlyRemovedListeners++; - } else { - // When we are outside the notifyListeners iterations we can - // effectively shrink the list. - _removeAt(i); - } - break; - } - } - } - - /// Discards any resources used by the object. - /// - /// After this is called, the object is not in a usable state and should be - /// discarded (calls to [addListener] will throw after the object is - /// disposed). - /// - /// This method should only be called by the object's owner. - /// - /// This method does not notify listeners, and clears the listener list once - /// it is called. Consumers of this class must decide on whether to notify - /// listeners or not immediately before disposal. - @mustCallSuper - void dispose() { - assert(_ChangeNotifier.debugAssertNotDisposed(this)); - assert( - _notificationCallStackDepth == 0, - 'The "dispose()" method on $this was called during the call to ' - '"notifyListeners()". This is likely to cause errors since it modifies ' - 'the list of listeners while the list is being used.', - ); - assert(() { - _debugDisposed = true; - return true; - }()); - _listeners = _emptyListeners; - _count = 0; - } - - /// Call all the registered listeners. - /// - /// Call this method whenever the object changes, to notify any clients the - /// object may have changed. Listeners that are added during this iteration - /// will not be visited. Listeners that are removed during this iteration will - /// not be visited after they are removed. - /// - /// Exceptions thrown by listeners will be caught and reported using - /// [FrameworkErrorReporter.instance]. - /// - /// This method must not be called after [dispose] has been called. - /// - /// Surprising behavior can result when reentrantly removing a listener (e.g. - /// in response to a notification) that has been registered multiple times. - /// See the discussion at [removeListener]. - @protected - @visibleForTesting - @pragma('vm:notify-debugger-on-exception') - void notifyListeners() { - assert(_ChangeNotifier.debugAssertNotDisposed(this)); - if (_count == 0) { - return; - } - - // To make sure that listeners removed during this iteration are not called, - // we set them to null, but we don't shrink the list right away. - // By doing this, we can continue to iterate on our list until it reaches - // the last listener added before the call to this method. - - // To allow potential listeners to recursively call notifyListener, we track - // the number of times this method is called in _notificationCallStackDepth. - // Once every recursive iteration is finished (i.e. when - // _notificationCallStackDepth == 0), - // we can safely shrink our list so that it will only contain not null - // listeners. - - _notificationCallStackDepth++; - - final int end = _count; - for (var i = 0; i < end; i++) { - try { - _listeners[i]?.call(); - } catch (exception, stack) { - FrameworkErrorReporter.instance.report( - FrameworkErrorDetails( - exception: exception, - stack: stack, - dispatchingObject: runtimeType, - ), - ); - } - } - - _notificationCallStackDepth--; - - if (_notificationCallStackDepth == 0 && _reentrantlyRemovedListeners > 0) { - // We really remove the listeners when all notifications are done. - final int newLength = _count - _reentrantlyRemovedListeners; - if (newLength * 2 <= _listeners.length) { - // As in _removeAt, we only shrink the list when the real number of - // listeners is half the length of our list. - final newListeners = List.filled(newLength, null); - - var newIndex = 0; - for (var i = 0; i < _count; i++) { - final VoidCallback? listener = _listeners[i]; - if (listener != null) { - newListeners[newIndex++] = listener; - } - } - - _listeners = newListeners; - } else { - // Otherwise we put all the null references at the end. - for (var i = 0; i < newLength; i += 1) { - if (_listeners[i] == null) { - // We swap this item with the next not null item. - int swapIndex = i + 1; - while (_listeners[swapIndex] == null) { - swapIndex += 1; - } - _listeners[i] = _listeners[swapIndex]; - _listeners[swapIndex] = null; - } - } - } - - _reentrantlyRemovedListeners = 0; - _count = newLength; - } - } -} - -/// A [_ChangeNotifier] that holds a single value. +/// A [ChangeNotifier] that holds a single value. /// /// When [value] is replaced with a new value that is **not equal** to the old /// value as evaluated by the equality operator (`==`), this class notifies its @@ -385,10 +32,9 @@ class _ChangeNotifier implements Listenable { /// /// Because of this behavior, [ValueNotifier] is best used with immutable data /// types. -class ValueNotifier implements ValueListenable, Listenable { - final _ChangeNotifier _changeNotifier = _ChangeNotifier(); - - /// Creates a [_ChangeNotifier] that wraps this value. +class ValueNotifier extends ChangeNotifier + implements ValueListenable, Listenable { + /// Creates a [ChangeNotifier] that wraps this value. ValueNotifier(this._value) { assert(() { if (kTrackMemoryLeaks) { @@ -427,12 +73,13 @@ class ValueNotifier implements ValueListenable, Listenable { return; } _value = newValue; - _changeNotifier.notifyListeners(); + notifyListeners(); } @override String toString() => '${describeIdentity(this)}($value)'; + @override void dispose() { assert(() { _debugDisposed = true; @@ -440,20 +87,6 @@ class ValueNotifier implements ValueListenable, Listenable { return true; }()); - _changeNotifier.dispose(); + super.dispose(); } - - @override - void addListener(VoidCallback listener) => - _changeNotifier.addListener(listener); - - @override - void removeListener(VoidCallback listener) => - _changeNotifier.removeListener(listener); - - @protected - bool get hasListeners => _changeNotifier.hasListeners; - - @protected - void notifyListeners() => _changeNotifier.notifyListeners(); } diff --git a/packages/ui_primitives/lib/ui_primitives.dart b/packages/ui_primitives/lib/ui_primitives.dart index 8d125171c..c590cdaa3 100644 --- a/packages/ui_primitives/lib/ui_primitives.dart +++ b/packages/ui_primitives/lib/ui_primitives.dart @@ -2,8 +2,9 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +export 'src/change_notifier.dart' show ChangeNotifier; export 'src/error_reporter.dart' show FrameworkErrorDetails, FrameworkErrorReporter; -export 'src/listenable.dart' show Listenable, ValueListenable; +export 'src/interfaces.dart' show Listenable, ValueListenable; export 'src/primitives.dart' show VoidCallback; export 'src/value_notifier.dart' show ValueNotifier; diff --git a/packages/ui_primitives/pubspec.yaml b/packages/ui_primitives/pubspec.yaml index df6c33aa7..bb8727386 100644 --- a/packages/ui_primitives/pubspec.yaml +++ b/packages/ui_primitives/pubspec.yaml @@ -5,7 +5,7 @@ name: ui_primitives description: Highly experimental package. repository: https://github.com/flutter/genui/tree/main/packages/ui_primitives -version: 0.2.0 +version: 0.0.1-dev-008 resolution: workspace