change_notifier.dart 6.42 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
/// A class that can be extended or mixed in that provides a change notification
/// API using [VoidCallback] for notifications.
37 38 39 40
///
/// [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).
Adam Barth's avatar
Adam Barth committed
41 42 43 44
///
/// See also:
///
///  * [ValueNotifier], which is a [ChangeNotifier] that wraps a single value.
45
class ChangeNotifier extends Listenable {
46
  ObserverList<VoidCallback> _listeners = new ObserverList<VoidCallback>();
47

48 49
  bool _debugAssertNotDisposed() {
    assert(() {
50
      if (_listeners == null) {
51 52 53 54 55 56 57 58 59 60
        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;
  }

61
  /// Register a closure to be called when the object changes.
62 63
  ///
  /// This method must not be called after [dispose] has been called.
64
  @override
65
  void addListener(VoidCallback listener) {
66
    assert(_debugAssertNotDisposed);
67 68 69 70 71
    _listeners.add(listener);
  }

  /// Remove a previously registered closure from the list of closures that are
  /// notified when the object changes.
72 73 74 75
  ///
  /// If the given listener is not registered, the call is ignored.
  ///
  /// This method must not be called after [dispose] has been called.
76 77 78 79 80 81 82 83 84 85 86 87 88
  ///
  /// 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.
89
  @override
90
  void removeListener(VoidCallback listener) {
91
    assert(_debugAssertNotDisposed);
92
    _listeners.remove(listener);
93 94
  }

95 96 97 98
  /// 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).
99 100 101 102
  ///
  /// This method should only be called by the object's owner.
  @mustCallSuper
  void dispose() {
103
    assert(_debugAssertNotDisposed);
104
    _listeners = null;
105 106 107 108 109
  }

  /// Call all the registered listeners.
  ///
  /// Call this method whenever the object changes, to notify any clients the
110 111 112
  /// 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.
113 114 115
  ///
  /// Exceptions thrown by listeners will be caught and reported using
  /// [FlutterError.reportError].
116 117
  ///
  /// This method must not be called after [dispose] has been called.
118 119 120 121
  ///
  /// 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].
122 123
  @protected
  void notifyListeners() {
124
    assert(_debugAssertNotDisposed);
125
    if (_listeners != null) {
126
      final List<VoidCallback> localListeners = new List<VoidCallback>.from(_listeners);
127
      for (VoidCallback listener in localListeners) {
128
        try {
129 130
          if (_listeners.contains(listener))
            listener();
131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146
        } 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
147 148 149 150

class _MergingListenable extends ChangeNotifier {
  _MergingListenable(this._children) {
    for (Listenable child in _children)
151
      child?.addListener(notifyListeners);
Ian Hickson's avatar
Ian Hickson committed
152 153 154 155 156 157 158
  }

  final List<Listenable> _children;

  @override
  void dispose() {
    for (Listenable child in _children)
159
      child?.removeListener(notifyListeners);
Ian Hickson's avatar
Ian Hickson committed
160 161
    super.dispose();
  }
162 163 164

  @override
  String toString() {
165
    return 'Listenable.merge([${_children.join(", ")}])';
166
  }
Ian Hickson's avatar
Ian Hickson committed
167
}
Adam Barth's avatar
Adam Barth committed
168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186

/// 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();
  }
187 188

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