// Copyright 2015 The Chromium 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 'package:meta/meta.dart'; import 'assertions.dart'; import 'basic_types.dart'; import 'diagnostics.dart'; import 'observer_list.dart'; /// An object that maintains a list of listeners. 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); } /// A class that can be extended or mixed in that provides a change notification /// API using [VoidCallback] for notifications. /// /// [ChangeNotifier] is optimised for small numbers (one or two) of listeners. /// It is O(N) for adding and removing listeners and O(N²) for dispatching /// notifications (where N is the number of listeners). /// /// See also: /// /// * [ValueNotifier], which is a [ChangeNotifier] that wraps a single value. class ChangeNotifier extends Listenable { ObserverList<VoidCallback> _listeners = new ObserverList<VoidCallback>(); bool _debugAssertNotDisposed() { assert(() { if (_listeners == null) { throw new 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; } /// Register a closure to be called when the object changes. /// /// This method must not be called after [dispose] has been called. @override void addListener(VoidCallback listener) { assert(_debugAssertNotDisposed()); _listeners.add(listener); } /// 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. /// /// If a listener had been added twice, and is removed once during an /// iteration (i.e. 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, and therefore conservatively /// still calling 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. @override void removeListener(VoidCallback listener) { assert(_debugAssertNotDisposed()); _listeners.remove(listener); } /// 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. 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 (i.e. /// in response to a notification) that has been registered multiple times. /// See the discussion at [removeListener]. @protected void notifyListeners() { assert(_debugAssertNotDisposed()); if (_listeners != null) { final List<VoidCallback> localListeners = new List<VoidCallback>.from(_listeners); for (VoidCallback listener in localListeners) { try { if (_listeners.contains(listener)) listener(); } catch (exception, stack) { FlutterError.reportError(new FlutterErrorDetails( exception: exception, stack: stack, library: 'foundation library', context: 'while dispatching notifications for $runtimeType', informationCollector: (StringBuffer information) { information.writeln('The $runtimeType sending notification was:'); information.write(' $this'); } )); } } } } } class _MergingListenable extends ChangeNotifier { _MergingListenable(this._children) { for (Listenable child in _children) child?.addListener(notifyListeners); } final List<Listenable> _children; @override void dispose() { for (Listenable child in _children) child?.removeListener(notifyListeners); super.dispose(); } @override String toString() { return 'Listenable.merge([${_children.join(", ")}])'; } } /// A [ChangeNotifier] that holds a single value. /// /// When [value] is replaced, this class notifies its listeners. class ValueNotifier<T> extends ChangeNotifier { /// Creates a [ChangeNotifier] that wraps this value. ValueNotifier(this._value); /// The current value stored in this notifier. /// /// When the value is replaced, this class notifies its listeners. T get value => _value; T _value; set value(T newValue) { if (_value == newValue) return; _value = newValue; notifyListeners(); } @override String toString() => '${describeIdentity(this)}($value)'; }