PathObserver class
A data-bound path starting from a view-model or model object, for example
foo.bar.baz
.
When the values stream is being listened to, this will observe changes to the object and any intermediate object along the path, and send values accordingly. When all listeners are unregistered it will stop observing the objects.
This class is used to implement Node.bind
and similar functionality.
class PathObserver { /** The object being observed. */ final object; /** The path string. */ final String path; /** True if the path is valid, otherwise false. */ final bool _isValid; // TODO(jmesserly): same issue here as ObservableMixin: is there an easier // way to get a broadcast stream? StreamController _values; Stream _valueStream; _PropertyObserver _observer, _lastObserver; Object _lastValue; bool _scheduled = false; /** * Observes [path] on [object] for changes. This returns an object that can be * used to get the changes and get/set the value at this path. * See [PathObserver.values] and [PathObserver.value]. */ PathObserver(this.object, String path) : path = path, _isValid = _isPathValid(path) { // TODO(jmesserly): if the path is empty, or the object is! Observable, we // can optimize the PathObserver to be more lightweight. _values = new StreamController.broadcast(sync: true, onListen: _observe, onCancel: _unobserve); if (_isValid) { var segments = []; for (var segment in path.trim().split('.')) { if (segment == '') continue; var index = int.parse(segment, onError: (_) {}); segments.add(index != null ? index : new Symbol(segment)); } // Create the property observer linked list. // Note that the structure of a path can't change after it is initially // constructed, even though the objects along the path can change. for (int i = segments.length - 1; i >= 0; i--) { _observer = new _PropertyObserver(this, segments[i], _observer); if (_lastObserver == null) _lastObserver = _observer; } } } // TODO(jmesserly): we could try adding the first value to the stream, but // that delivers the first record async. /** * Listens to the stream, and invokes the [callback] immediately with the * current [value]. This is useful for bindings, which want to be up-to-date * immediately. */ StreamSubscription bindSync(void callback(value)) { var result = values.listen(callback); callback(value); return result; } // TODO(jmesserly): should this be a change record with the old value? // TODO(jmesserly): should this be a broadcast stream? We only need // single-subscription in the bindings system, so single sub saves overhead. /** * Gets the stream of values that were observed at this path. * This returns a single-subscription stream. */ Stream get values => _values.stream; /** Force synchronous delivery of [values]. */ void _deliverValues() { _scheduled = false; var newValue = value; if (!identical(_lastValue, newValue)) { _values.add(newValue); _lastValue = newValue; } } void _observe() { if (_observer != null) { _lastValue = value; _observer.observe(); } } void _unobserve() { if (_observer != null) _observer.unobserve(); } void _notifyChange() { if (_scheduled) return; _scheduled = true; // TODO(jmesserly): should we have a guarenteed order with respect to other // paths? If so, we could implement this fairly easily by sorting instances // of this class by birth order before delivery. queueChangeRecords(_deliverValues); } /** Gets the last reported value at this path. */ get value { if (!_isValid) return null; if (_observer == null) return object; _observer.ensureValue(object); return _lastObserver.value; } /** Sets the value at this path. */ void set value(Object value) { // TODO(jmesserly): throw if property cannot be set? // MDV seems tolerant of these error. if (_observer == null || !_isValid) return; _observer.ensureValue(object); var last = _lastObserver; if (_setObjectProperty(last._object, last._property, value)) { // Technically, this would get updated asynchronously via a change record. // However, it is nice if calling the getter will yield the same value // that was just set. So we use this opportunity to update our cache. last.value = value; } } }
Constructors
new PathObserver(object, String path) #
Observes path on object for changes. This returns an object that can be used to get the changes and get/set the value at this path. See PathObserver.values and PathObserver.value.
PathObserver(this.object, String path) : path = path, _isValid = _isPathValid(path) { // TODO(jmesserly): if the path is empty, or the object is! Observable, we // can optimize the PathObserver to be more lightweight. _values = new StreamController.broadcast(sync: true, onListen: _observe, onCancel: _unobserve); if (_isValid) { var segments = []; for (var segment in path.trim().split('.')) { if (segment == '') continue; var index = int.parse(segment, onError: (_) {}); segments.add(index != null ? index : new Symbol(segment)); } // Create the property observer linked list. // Note that the structure of a path can't change after it is initially // constructed, even though the objects along the path can change. for (int i = segments.length - 1; i >= 0; i--) { _observer = new _PropertyObserver(this, segments[i], _observer); if (_lastObserver == null) _lastObserver = _observer; } } }
Properties
final object #
The object being observed.
final object
final String path #
The path string.
final String path
dynamic get value #
Gets the last reported value at this path.
get value { if (!_isValid) return null; if (_observer == null) return object; _observer.ensureValue(object); return _lastObserver.value; }
void set value(Object value) #
Sets the value at this path.
void set value(Object value) { // TODO(jmesserly): throw if property cannot be set? // MDV seems tolerant of these error. if (_observer == null || !_isValid) return; _observer.ensureValue(object); var last = _lastObserver; if (_setObjectProperty(last._object, last._property, value)) { // Technically, this would get updated asynchronously via a change record. // However, it is nice if calling the getter will yield the same value // that was just set. So we use this opportunity to update our cache. last.value = value; } }
final Stream values #
Gets the stream of values that were observed at this path. This returns a single-subscription stream.
Stream get values => _values.stream;
Methods
StreamSubscription bindSync(void callback(value)) #
Listens to the stream, and invokes the callback immediately with the current value. This is useful for bindings, which want to be up-to-date immediately.
StreamSubscription bindSync(void callback(value)) { var result = values.listen(callback); callback(value); return result; }