Last active
November 16, 2020 22:58
-
-
Save escamoteur/db80d0d065ac07e4a9d1cc0c37c1c779 to your computer and use it in GitHub Desktop.
change_notifier_manual.dart
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Copyright 2014 The Flutter Authors. All rights reserved. | |
// Use of this source code is governed by a BSD-style license that can be | |
// found in the LICENSE file. | |
import 'dart:collection'; | |
import 'dart:developer'; | |
import 'package:meta/meta.dart'; | |
import 'assertions.dart'; | |
import 'basic_types.dart'; | |
import 'diagnostics.dart'; | |
/// An object that maintains a list of listeners. | |
/// | |
/// The listeners are typically used to notify clients that the object has been | |
/// updated. | |
/// | |
/// There are two variants of this interface: | |
/// | |
/// * [ValueListenable], an interface that augments the [Listenable] interface | |
/// with the concept of a _current value_. | |
/// | |
/// * [Animation], an interface that augments the [ValueListenable] interface | |
/// to add the concept of direction (forward or reverse). | |
/// | |
/// Many classes in the Flutter API use or implement these interfaces. The | |
/// following subclasses are especially relevant: | |
/// | |
/// * [ChangeNotifier], which can be subclassed or mixed in to create objects | |
/// that implement the [Listenable] interface. | |
/// | |
/// * [ValueNotifier], which implements the [ValueListenable] interface with | |
/// a mutable value that triggers the notifications when modified. | |
/// | |
/// The terms "notify clients", "send notifications", "trigger notifications", | |
/// and "fire notifications" are used interchangeably. | |
/// | |
/// See also: | |
/// | |
/// * [AnimatedBuilder], a widget that uses a builder callback to rebuild | |
/// whenever a given [Listenable] triggers its notifications. This widget is | |
/// commonly used with [Animation] subclasses, hence its name, but is by no | |
/// means limited to animations, as it can be used with any [Listenable]. It | |
/// is a subclass of [AnimatedWidget], which can be used to create widgets | |
/// that are driven from a [Listenable]. | |
/// * [ValueListenableBuilder], a widget that uses a builder callback to | |
/// rebuild whenever a [ValueListenable] object triggers its notifications, | |
/// providing the builder with the value of the object. | |
/// * [InheritedNotifier], an abstract superclass for widgets that use a | |
/// [Listenable]'s notifications to trigger rebuilds in descendant widgets | |
/// that declare a dependency on them, using the [InheritedWidget] mechanism. | |
/// * [new Listenable.merge], which creates a [Listenable] that triggers | |
/// notifications whenever any of a list of other [Listenable]s trigger their | |
/// notifications. | |
abstract class Listenable { | |
/// Abstract const constructor. This constructor enables subclasses to provide | |
/// const constructors so that they can be used in const expressions. | |
const Listenable(); | |
/// Return a [Listenable] that triggers when any of the given [Listenable]s | |
/// themselves trigger. | |
/// | |
/// The list must not be changed after this method has been called. Doing so | |
/// will lead to memory leaks or exceptions. | |
/// | |
/// The list may contain nulls; they are ignored. | |
factory Listenable.merge(List<Listenable?> listenables) = _MergingListenable; | |
/// Register a closure to be called when the object notifies its listeners. | |
void addListener(VoidCallback listener); | |
/// Remove a previously registered closure from the list of closures that the | |
/// object notifies. | |
void removeListener(VoidCallback listener); | |
} | |
/// An interface for subclasses of [Listenable] that expose a [value]. | |
/// | |
/// This interface is implemented by [ValueNotifier<T>] and [Animation<T>], and | |
/// allows other APIs to accept either of those implementations interchangeably. | |
/// | |
/// See also: | |
/// | |
/// * [ValueListenableBuilder], a widget that uses a builder callback to | |
/// rebuild whenever a [ValueListenable] object triggers its notifications, | |
/// providing the builder with the value of the object. | |
abstract class ValueListenable<T> extends Listenable { | |
/// Abstract const constructor. This constructor enables subclasses to provide | |
/// const constructors so that they can be used in const expressions. | |
const ValueListenable(); | |
/// The current value of the object. When the value changes, the callbacks | |
/// registered with [addListener] will be invoked. | |
T get value; | |
} | |
class _ListenerEntry extends LinkedListEntry<_ListenerEntry> { | |
_ListenerEntry(this.listener, this.id); | |
final String id; | |
final VoidCallback listener; | |
} | |
/// 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). | |
/// | |
/// See also: | |
/// | |
/// * [ValueNotifier], which is a [ChangeNotifier] that wraps a single value. | |
class ChangeNotifier implements Listenable { | |
LinkedList<_ListenerEntry>? _listeners = LinkedList<_ListenerEntry>(); | |
_ListenerEntry? _notifyCursor; | |
_ListenerEntry? _notifyLimit; | |
bool _debugAssertNotDisposed() { | |
assert(() { | |
if (_listeners == null) { | |
throw FlutterError('A $runtimeType was used after being disposed.\n' | |
'Once you have called dispose() on a $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. | |
@protected | |
bool get hasListeners { | |
assert(_debugAssertNotDisposed()); | |
return _listeners?.isNotEmpty ?? false; | |
} | |
/// 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, [String id = '']) { | |
assert(_debugAssertNotDisposed()); | |
// if (_notifyCursor != null) { | |
// _listenersToAddAfterNotify.add(_ListenerEntry(listener, id)); | |
// } else { | |
// _listeners?.add(_ListenerEntry(listener, id)); | |
// } | |
if (_notifyCursor != null) { | |
_listeners?.add(_ListenerEntry(listener, id)); | |
} else { | |
_listeners?.add(_ListenerEntry(listener, id)); | |
} | |
} | |
void printListeners(String prefix) { | |
String s; | |
s = '$prefix : '; | |
for (var p = _listeners?.first; p != null; p = p.next) { | |
s += p.id; | |
} | |
s += ' - cursor: ${_notifyCursor?.id} - ${_notifyLimit?.id}'; | |
print(s); | |
} | |
/// 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 must not be called after [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) { | |
assert(_debugAssertNotDisposed()); | |
if (_listeners!.isEmpty) return; | |
for (_ListenerEntry? entry = _listeners?.first; | |
entry != null; | |
entry = entry.next) { | |
if (entry.listener == listener) { | |
if (entry == _notifyLimit) { | |
_notifyLimit = _notifyLimit?.previous; | |
if (entry == _notifyCursor) { | |
_notifyCursor = _notifyLimit; | |
} | |
} else { | |
if (entry == _notifyCursor) { | |
_notifyCursor = _notifyCursor?.previous; | |
} | |
} | |
entry.unlink(); | |
return; | |
} | |
} | |
} | |
/// 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] and [removeListener] will throw after the object is | |
/// disposed). | |
/// | |
/// This method should only be called by the object's owner. | |
@mustCallSuper | |
void dispose() { | |
assert(_debugAssertNotDisposed()); | |
_listeners = null; | |
} | |
/// 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 | |
void notifyListeners() { | |
assert(_debugAssertNotDisposed()); | |
if (_listeners!.isEmpty) return; | |
_notifyLimit = _listeners?.last; | |
_notifyCursor = _listeners?.first; | |
do { | |
try { | |
_notifyCursor?.listener(); | |
} catch (exception, stack) { | |
FlutterError.reportError(FlutterErrorDetails( | |
exception: exception, | |
stack: stack, | |
library: 'foundation library', | |
context: ErrorDescription( | |
'while dispatching notifications for $runtimeType'), | |
informationCollector: () sync* { | |
yield DiagnosticsProperty<ChangeNotifier>( | |
'The $runtimeType sending notification was', | |
this, | |
style: DiagnosticsTreeStyle.errorProperty, | |
); | |
}, | |
)); | |
} | |
/// In case that the first listener removes itself | |
/// _notifyCursor could be null here. | |
if (_notifyCursor == null) { | |
/// in case that the List is now empty | |
if (_listeners!.isEmpty) return; | |
_notifyCursor = _listeners?.first; | |
} else { | |
_notifyCursor = _notifyCursor?.next; | |
} | |
} while (_notifyCursor != null && _notifyCursor?.previous != _notifyLimit); | |
_notifyCursor = null; | |
_notifyLimit = null; | |
} | |
} | |
class _MergingListenable extends Listenable { | |
_MergingListenable(this._children); | |
final List<Listenable?> _children; | |
@override | |
void addListener(VoidCallback listener) { | |
for (final Listenable? child in _children) { | |
child?.addListener(listener); | |
} | |
} | |
@override | |
void removeListener(VoidCallback listener) { | |
for (final Listenable? child in _children) { | |
child?.removeListener(listener); | |
} | |
} | |
@override | |
String toString() { | |
return 'Listenable.merge([${_children.join(", ")}])'; | |
} | |
} | |
/// A [ChangeNotifier] that holds a single value. | |
/// | |
/// When [value] is replaced with something that is not equal to the old | |
/// value as evaluated by the equality operator ==, this class notifies its | |
/// listeners. | |
class ValueNotifier<T> extends ChangeNotifier implements ValueListenable<T> { | |
/// Creates a [ChangeNotifier] that wraps this value. | |
ValueNotifier(this._value); | |
/// The current value stored in this notifier. | |
/// | |
/// When the value is replaced with something that is not equal to the old | |
/// value as evaluated by the equality operator ==, this class notifies its | |
/// listeners. | |
@override | |
T get value => _value; | |
T _value; | |
set value(T newValue) { | |
if (_value == newValue) return; | |
_value = newValue; | |
notifyListeners(); | |
} | |
@override | |
String toString() => '${describeIdentity(this)}($value)'; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment