Line data Source code
1 : import 'package:flutter/foundation.dart'; 2 : 3 : /// Signature of callbacks that have 1 argument and return no data. 4 : typedef PropertyCallback<T> = void Function(T); 5 : 6 : /// A backwards-compatible implementation of [ChangeNotifier] that allows 7 : /// implementers to provide more granular information to listeners about what 8 : /// specific property was changed. This lets listeners be much more efficient 9 : /// when responding to model changes. Any number of listeners can subscribe to 10 : /// any number of properties. 11 : /// 12 : /// Like [ChangeNotifier], is optimized for small numbers (one or two) of listeners. 13 : /// It is O(N) for adding and removing listeners and O(N²) for dispatching 14 : /// notifications (where N is the number of listeners). 15 : /// 16 : /// [T] is the type of the property name and is usually [String] but can 17 : /// be an [Enum] or any type that subclasses [Object]. To work correctly, 18 : /// [T] must implement `operator==` and `hashCode`. 19 : class PropertyChangeNotifier<T extends Object> extends ChangeNotifier { 20 : var _globalListeners = ObserverList<Function>(); 21 : var _propertyListeners = <T, ObserverList<Function>>{}; 22 : 23 : /// Reimplemented from [ChangeNotifier]. 24 : /// Clients should not depend on this value for their behavior, because having 25 : /// one listener's logic change when another listener happens to start or stop 26 : /// listening will lead to extremely hard-to-track bugs. Subclasses might use 27 : /// this information to determine whether to do any work when there are no 28 : /// listeners, however; for example, resuming a [Stream] when a listener is 29 : /// added and pausing it when a listener is removed. 30 : /// 31 : /// Typically this is used by overriding [addListener], checking if 32 : /// [hasListeners] is false before calling `super.addListener()`, and if so, 33 : /// starting whatever work is needed to determine when to call 34 : /// [notifyListeners]; and similarly, by overriding [removeListener], checking 35 : /// if [hasListeners] is false after calling `super.removeListener()`, and if 36 : /// so, stopping that same work. 37 1 : @override 38 : @protected 39 : @visibleForTesting 40 : bool get hasListeners { 41 1 : assert(_debugAssertNotDisposed()); 42 4 : return _globalListeners.isNotEmpty || _propertyListeners.isNotEmpty; 43 : } 44 : 45 : /// Registers [listener] for the given [properties]. [listener] must not be null. 46 : /// If [properties] is null or empty, [listener] will be added as a global listener, meaning 47 : /// it will be invoked for all property changes. This is the default behavior of [ChangeNotifier]. 48 : /// [listener] must either accept no parameters or a single [T] parameter. If [listener] 49 : /// accepts a [T] parameter, it will be invoked with the property name provided by [notifyListeners]. 50 : /// The same [listener] can be added for multiple properties. 51 : /// Adding the same [listener] for the same property is a no-op. 52 : /// Adding a [listener] for a non-existent property will not fail, but is functionally pointless. 53 3 : @override 54 : void addListener(Function listener, [Iterable<T> properties]) { 55 3 : assert(_debugAssertNotDisposed()); 56 1 : assert(listener != null); 57 7 : assert(listener is VoidCallback || listener is PropertyCallback<T>, 'Listener must be a Function() or Function(T)'); 58 : 59 : // Register global listener only 60 1 : if (properties == null || properties.isEmpty) { 61 6 : _addListener(_globalListeners, listener); 62 : return; 63 : } 64 : 65 : // Register listener for every property 66 2 : for (final property in properties) { 67 2 : if (!_propertyListeners.containsKey(property)) { 68 3 : _propertyListeners[property] = ObserverList<Function>(); 69 : } 70 3 : _addListener(_propertyListeners[property], listener); 71 : } 72 : } 73 : 74 : /// Removes [listener] for the given [properties]. [listener] must not be null. 75 : /// If [properties] is null or empty, [listener] will be removed as a global listener. 76 : /// Removing a listener will not affect any other properties [listeners] is registered for. 77 : /// Removing a non-existent listener is no-op. 78 : /// Removing a listener for a non-existent property will not fail. 79 3 : @override 80 : void removeListener(Function listener, [Iterable<T> properties]) { 81 3 : assert(_debugAssertNotDisposed()); 82 1 : assert(listener != null); 83 : 84 : // Remove global listener only 85 1 : if (properties == null || properties.isEmpty) { 86 6 : _globalListeners.remove(listener); 87 : return; 88 : } 89 : 90 : // Remove listener for every property 91 2 : for (final property in properties) { 92 : // If no map entry exists for property, ignore 93 2 : if (!_propertyListeners.containsKey(property)) { 94 : continue; 95 : } 96 : 97 : // Remove listener 98 2 : final listeners = _propertyListeners[property]; 99 1 : listeners.remove(listener); 100 : 101 : // Remove map entry if needed 102 1 : if (listeners.isEmpty) { 103 2 : _propertyListeners.remove(property); 104 : } 105 : } 106 : } 107 : 108 : /// Reimplemented from [ChangeNotifier]. 109 : /// Discards any resources used by the object. After this is called, the 110 : /// object is not in a usable state and should be discarded (calls to 111 : /// [addListener] and [removeListener] will throw after the object is 112 : /// disposed). 113 : /// 114 : /// This method should only be called by the object's owner. 115 1 : @override 116 : @mustCallSuper 117 : void dispose() { 118 1 : assert(_debugAssertNotDisposed()); 119 1 : _globalListeners = null; 120 1 : _propertyListeners = null; 121 1 : super.dispose(); 122 : } 123 : 124 : /// Notifies the appropriate listeners that [property] was changed. 125 : /// Implementers should ideally provide a [property] parameter. 126 : /// It is only optional for backwards compatibility with [ChangeNotifier]. 127 : /// Global listeners will be notified every time, even if [property] is null. 128 : /// Listeners for specific properties will only be notified 129 : /// if [property] is equal (operator==) to one of those properties. 130 : /// If [property] is not null, must be a single instance of [T] (typically a [String]). 131 3 : @override 132 : @protected 133 : @visibleForTesting 134 : void notifyListeners([T property]) { 135 3 : assert(_debugAssertNotDisposed()); 136 4 : assert(!(property is Iterable), 'notifyListeners() should only be called for one property at a time'); 137 : 138 : // Always notify global listeners 139 6 : _notifyListeners(_globalListeners, property); 140 : 141 : // If no property provided, exit 142 : if (property == null) { 143 : return; 144 : } 145 : 146 : // If listeners exist for this property, notify them. 147 6 : if (_propertyListeners.containsKey(property)) { 148 3 : _notifyListeners(_propertyListeners[property], property); 149 : } 150 : } 151 : 152 : /// Adds [listener] to [listeners] only if is not already present. 153 3 : void _addListener(ObserverList<Function> listeners, Function listener) { 154 3 : if (!listeners.contains(listener)) { 155 3 : listeners.add(listener); 156 : } 157 : } 158 : 159 : /// Creates a local copy of [listeners] in case a callback calls 160 : /// [addListener] or [removeListener] while iterating through the list. 161 : /// Invokes each listener. If the listener accepts a property parameter, it will be provided. 162 3 : void _notifyListeners(ObserverList<Function> listeners, T property) { 163 3 : final localListeners = List<Function>.from(listeners); 164 6 : for (final listener in localListeners) { 165 : // One last check to make sure the listener hasn't been removed 166 : // from the original list since the time we made our local copy. 167 3 : if (listeners.contains(listener)) { 168 3 : if (listener is PropertyCallback<T>) { 169 3 : listener(property); 170 : } else { 171 1 : listener(); 172 : } 173 : } 174 : } 175 : } 176 : 177 : /// Reimplemented from [ChangeNotifier]. 178 3 : bool _debugAssertNotDisposed() { 179 3 : assert(() { 180 6 : if (_globalListeners == null || _propertyListeners == null) { 181 2 : throw FlutterError('A $runtimeType was used after being disposed.\n' 182 1 : 'Once you have called dispose() on a $runtimeType, it can no longer be used.'); 183 : } 184 : return true; 185 3 : }()); 186 : return true; 187 : } 188 : }