change_notifier.dart 7.92 KB
Newer Older
1 2 3 4
// 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.

5
import 'package:meta/meta.dart';
6 7 8

import 'assertions.dart';
import 'basic_types.dart';
9
import 'diagnostics.dart';
10
import 'observer_list.dart';
Ian Hickson's avatar
Ian Hickson committed
11 12 13 14 15 16 17 18 19 20 21 22

/// 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.
23
  ///
24
  /// The list may contain nulls; they are ignored.
Ian Hickson's avatar
Ian Hickson committed
25 26 27 28 29 30 31 32 33
  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);
}
34

35 36 37 38 39 40 41 42 43 44
/// 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.
abstract class ValueListenable<T> extends Listenable {
  /// The current value of the object. When the value changes, the callbacks
  /// registered with [addListener] will be invoked.
  T get value;
}

45 46
/// A class that can be extended or mixed in that provides a change notification
/// API using [VoidCallback] for notifications.
47
///
48
/// [ChangeNotifier] is optimized for small numbers (one or two) of listeners.
49 50
/// It is O(N) for adding and removing listeners and O(N²) for dispatching
/// notifications (where N is the number of listeners).
Adam Barth's avatar
Adam Barth committed
51 52 53 54
///
/// See also:
///
///  * [ValueNotifier], which is a [ChangeNotifier] that wraps a single value.
55
class ChangeNotifier extends Listenable {
56
  ObserverList<VoidCallback> _listeners = new ObserverList<VoidCallback>();
57

58 59
  bool _debugAssertNotDisposed() {
    assert(() {
60
      if (_listeners == null) {
61 62 63 64 65 66
        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;
67
    }());
68 69 70
    return true;
  }

71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91
  /// 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;
  }

92
  /// Register a closure to be called when the object changes.
93 94
  ///
  /// This method must not be called after [dispose] has been called.
95
  @override
96
  void addListener(VoidCallback listener) {
97
    assert(_debugAssertNotDisposed());
98 99 100 101 102
    _listeners.add(listener);
  }

  /// Remove a previously registered closure from the list of closures that are
  /// notified when the object changes.
103 104 105 106
  ///
  /// If the given listener is not registered, the call is ignored.
  ///
  /// This method must not be called after [dispose] has been called.
107 108 109 110 111 112 113 114 115 116 117 118 119
  ///
  /// 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.
120
  @override
121
  void removeListener(VoidCallback listener) {
122
    assert(_debugAssertNotDisposed());
123
    _listeners.remove(listener);
124 125
  }

126 127 128 129
  /// 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).
130 131 132 133
  ///
  /// This method should only be called by the object's owner.
  @mustCallSuper
  void dispose() {
134
    assert(_debugAssertNotDisposed());
135
    _listeners = null;
136 137 138 139 140
  }

  /// Call all the registered listeners.
  ///
  /// Call this method whenever the object changes, to notify any clients the
141 142 143
  /// 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.
144 145 146
  ///
  /// Exceptions thrown by listeners will be caught and reported using
  /// [FlutterError.reportError].
147 148
  ///
  /// This method must not be called after [dispose] has been called.
149 150 151 152
  ///
  /// 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].
153 154
  @protected
  void notifyListeners() {
155
    assert(_debugAssertNotDisposed());
156
    if (_listeners != null) {
157
      final List<VoidCallback> localListeners = new List<VoidCallback>.from(_listeners);
158
      for (VoidCallback listener in localListeners) {
159
        try {
160 161
          if (_listeners.contains(listener))
            listener();
162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177
        } 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');
            }
          ));
        }
      }
    }
  }
}
Ian Hickson's avatar
Ian Hickson committed
178 179 180 181

class _MergingListenable extends ChangeNotifier {
  _MergingListenable(this._children) {
    for (Listenable child in _children)
182
      child?.addListener(notifyListeners);
Ian Hickson's avatar
Ian Hickson committed
183 184 185 186 187 188 189
  }

  final List<Listenable> _children;

  @override
  void dispose() {
    for (Listenable child in _children)
190
      child?.removeListener(notifyListeners);
Ian Hickson's avatar
Ian Hickson committed
191 192
    super.dispose();
  }
193 194 195

  @override
  String toString() {
196
    return 'Listenable.merge([${_children.join(", ")}])';
197
  }
Ian Hickson's avatar
Ian Hickson committed
198
}
Adam Barth's avatar
Adam Barth committed
199 200 201 202

/// A [ChangeNotifier] that holds a single value.
///
/// When [value] is replaced, this class notifies its listeners.
203
class ValueNotifier<T> extends ChangeNotifier implements ValueListenable<T> {
Adam Barth's avatar
Adam Barth committed
204 205 206 207 208 209
  /// 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.
210
  @override
Adam Barth's avatar
Adam Barth committed
211 212 213 214 215 216 217 218
  T get value => _value;
  T _value;
  set value(T newValue) {
    if (_value == newValue)
      return;
    _value = newValue;
    notifyListeners();
  }
219 220

  @override
221
  String toString() => '${describeIdentity(this)}($value)';
Adam Barth's avatar
Adam Barth committed
222
}