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