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 : ObserverList<Function>? _globalListeners = ObserverList<Function>(); 21 : Map<T, ObserverList<Function>>? _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 7 : assert(listener is VoidCallback || listener is PropertyCallback<T>, 'Listener must be a Function() or Function(T?)'); 57 : 58 : // Register global listener only 59 1 : if (properties == null || properties.isEmpty) { 60 6 : _addListener(_globalListeners!, listener); 61 : return; 62 : } 63 : 64 : // Register listener for every property 65 2 : for (final property in properties) { 66 2 : if (!_propertyListeners!.containsKey(property)) { 67 3 : _propertyListeners![property] = ObserverList<Function>(); 68 : } 69 3 : _addListener(_propertyListeners![property]!, listener); 70 : } 71 : } 72 : 73 : /// Removes [listener] for the given [properties]. [listener] must not be null. 74 : /// If [properties] is null or empty, [listener] will be removed as a global listener. 75 : /// Removing a listener will not affect any other properties [listeners] is registered for. 76 : /// Removing a non-existent listener is no-op. 77 : /// Removing a listener for a non-existent property will not fail. 78 3 : @override 79 : void removeListener(Function listener, [Iterable<T>? properties]) { 80 3 : assert(_debugAssertNotDisposed()); 81 : 82 : // Remove global listener only 83 1 : if (properties == null || properties.isEmpty) { 84 6 : _globalListeners!.remove(listener); 85 : return; 86 : } 87 : 88 : // Remove listener for every property 89 2 : for (final property in properties) { 90 : // If no map entry exists for property, ignore 91 2 : if (!_propertyListeners!.containsKey(property)) { 92 : continue; 93 : } 94 : 95 : // Remove listener 96 2 : final listeners = _propertyListeners![property]!; 97 1 : listeners.remove(listener); 98 : 99 : // Remove map entry if needed 100 1 : if (listeners.isEmpty) { 101 2 : _propertyListeners!.remove(property); 102 : } 103 : } 104 : } 105 : 106 : /// Reimplemented from [ChangeNotifier]. 107 : /// Discards any resources used by the object. After this is called, the 108 : /// object is not in a usable state and should be discarded (calls to 109 : /// [addListener] and [removeListener] will throw after the object is 110 : /// disposed). 111 : /// 112 : /// This method should only be called by the object's owner. 113 1 : @override 114 : @mustCallSuper 115 : void dispose() { 116 1 : assert(_debugAssertNotDisposed()); 117 1 : _globalListeners = null; 118 1 : _propertyListeners = null; 119 1 : super.dispose(); 120 : } 121 : 122 : /// Notifies the appropriate listeners that [property] was changed. 123 : /// Implementers should ideally provide a [property] parameter. 124 : /// It is only optional for backwards compatibility with [ChangeNotifier]. 125 : /// Global listeners will be notified every time, even if [property] is null. 126 : /// Listeners for specific properties will only be notified 127 : /// if [property] is equal (operator==) to one of those properties. 128 : /// If [property] is not null, must be a single instance of [T] (typically a [String]). 129 3 : @override 130 : @protected 131 : @visibleForTesting 132 : void notifyListeners([T? property]) { 133 3 : assert(_debugAssertNotDisposed()); 134 4 : assert(property is! Iterable, 'notifyListeners() should only be called for one property at a time'); 135 : 136 : // Always notify global listeners 137 6 : _notifyListeners(_globalListeners!, property); 138 : 139 : // If no property provided, exit 140 : if (property == null) { 141 : return; 142 : } 143 : 144 : // If listeners exist for this property, notify them. 145 6 : if (_propertyListeners!.containsKey(property)) { 146 3 : _notifyListeners(_propertyListeners![property]!, property); 147 : } 148 : } 149 : 150 : /// Adds [listener] to [listeners] only if is not already present. 151 3 : void _addListener(ObserverList<Function> listeners, Function listener) { 152 3 : if (!listeners.contains(listener)) { 153 3 : listeners.add(listener); 154 : } 155 : } 156 : 157 : /// Creates a local copy of [listeners] in case a callback calls 158 : /// [addListener] or [removeListener] while iterating through the list. 159 : /// Invokes each listener. If the listener accepts a property parameter, it will be provided. 160 3 : void _notifyListeners(ObserverList<Function> listeners, T? property) { 161 3 : final localListeners = List<Function>.from(listeners); 162 6 : for (final listener in localListeners) { 163 : // One last check to make sure the listener hasn't been removed 164 : // from the original list since the time we made our local copy. 165 3 : if (listeners.contains(listener)) { 166 3 : if (listener is PropertyCallback<T>) { 167 3 : listener(property); 168 : } else { 169 1 : listener(); 170 : } 171 : } 172 : } 173 : } 174 : 175 : /// Reimplemented from [ChangeNotifier]. 176 3 : bool _debugAssertNotDisposed() { 177 3 : assert(() { 178 6 : if (_globalListeners == null || _propertyListeners == null) { 179 3 : throw FlutterError('A $runtimeType was used after being disposed.\n' 180 1 : 'Once you have called dispose() on a $runtimeType, it can no longer be used.'); 181 : } 182 : return true; 183 3 : }()); 184 : return true; 185 : } 186 : } 187 : 188 : /// A convenience typedef to use in the common use case where property names are of type [String]. 189 : typedef StringPropertyChangeNotifier = PropertyChangeNotifier<String>;