| // Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file |
| // for details. All rights reserved. Use of this source code is governed by a |
| // BSD-style license that can be found in the LICENSE file. |
| |
| part of observe; |
| |
| /** |
| * Use `@observable` to make a field automatically observable. |
| */ |
| const Object observable = const _ObservableAnnotation(); |
| |
| /** |
| * Interface representing an observable object. This is used by data in |
| * model-view architectures to notify interested parties of [changes]. |
| * |
| * This object does not require any specific technique to implement |
| * observability. If you mixin [ObservableMixin], [dirtyCheck] will know to |
| * check for changes on the object. You may also implement change notification |
| * yourself, by calling [notifyChange]. |
| * |
| * You can use [ObservableBase] or [ObservableMixin] to implement this. |
| */ |
| abstract class Observable { |
| /** |
| * The stream of change records to this object. Records will be delivered |
| * asynchronously. |
| * |
| * [deliverChanges] can be called to force synchronous delivery. |
| */ |
| Stream<List<ChangeRecord>> get changes; |
| |
| /** |
| * Synchronously deliver pending [changes]. Returns true if any records were |
| * delivered, otherwise false. |
| */ |
| // TODO(jmesserly): this is a bit different from the ES Harmony version, which |
| // allows delivery of changes to a particular observer: |
| // http://wiki.ecmascript.org/doku.php?id=harmony:observe#object.deliverchangerecords |
| // |
| // The rationale for that, and for async delivery in general, is the principal |
| // that you shouldn't run code (observers) when it doesn't expect to be run. |
| // If you do that, you risk violating invariants that the code assumes. |
| // |
| // For this reason, we need to match the ES Harmony version. The way we can do |
| // this in Dart is to add a method on StreamSubscription (possibly by |
| // subclassing Stream* types) that immediately delivers records for only |
| // that subscription. Alternatively, we could consider using something other |
| // than Stream to deliver the multicast change records, and provide an |
| // Observable->Stream adapter. |
| // |
| // Also: we should be delivering changes to the observer (subscription) based |
| // on the birth order of the observer. This is for compatibility with ES |
| // Harmony as well as predictability for app developers. |
| bool deliverChanges(); |
| |
| /** |
| * Notify observers of a change. |
| * |
| * For most objects [ObservableMixin.notifyPropertyChange] is more |
| * convenient, but collections sometimes deliver other types of changes such |
| * as a [ListChangeRecord]. |
| */ |
| void notifyChange(ChangeRecord record); |
| |
| /** |
| * True if this object has any observers, and should call |
| * [notifyChange] for changes. |
| */ |
| bool get hasObservers; |
| |
| /** |
| * Performs dirty checking of objects that inherit from [ObservableMixin]. |
| * This scans all observed objects using mirrors and determines if any fields |
| * have changed. If they have, it delivers the changes for the object. |
| */ |
| static void dirtyCheck() => dirtyCheckObservables(); |
| } |
| |
| /** |
| * Base class implementing [Observable]. |
| * |
| * When a field, property, or indexable item is changed, the change record |
| * will be sent to [changes]. |
| */ |
| typedef ObservableBase = Object with ObservableMixin; |
| |
| /** |
| * Mixin for implementing [Observable] objects. |
| * |
| * When a field, property, or indexable item is changed, the change record |
| * will be sent to [changes]. |
| */ |
| abstract class ObservableMixin implements Observable { |
| StreamController _changes; |
| InstanceMirror _mirror; |
| |
| Map<Symbol, Object> _values; |
| List<ChangeRecord> _records; |
| |
| static final _objectType = reflectClass(Object); |
| |
| Stream<List<ChangeRecord>> get changes { |
| if (_changes == null) { |
| _changes = new StreamController.broadcast(sync: true, |
| onListen: _observed, onCancel: _unobserved); |
| } |
| return _changes.stream; |
| } |
| |
| bool get hasObservers => _changes != null && _changes.hasListener; |
| |
| void _observed() { |
| // Register this object for dirty checking purposes. |
| registerObservable(this); |
| |
| var mirror = reflect(this); |
| var values = new Map<Symbol, Object>(); |
| |
| // Note: we scan for @observable regardless of whether the base type |
| // actually includes this mixin. While perhaps too inclusive, it lets us |
| // avoid complex logic that walks "with" and "implements" clauses. |
| for (var type = mirror.type; type != _objectType; type = type.superclass) { |
| for (var field in type.variables.values) { |
| if (field.isFinal || field.isStatic || field.isPrivate) continue; |
| |
| for (var meta in field.metadata) { |
| if (identical(observable, meta.reflectee)) { |
| var name = field.simpleName; |
| // Note: since this is a field, getting the value shouldn't execute |
| // user code, so we don't need to worry about errors. |
| values[name] = mirror.getField(name).reflectee; |
| break; |
| } |
| } |
| } |
| } |
| |
| _mirror = mirror; |
| _values = values; |
| } |
| |
| /** Release data associated with observation. */ |
| void _unobserved() { |
| // Note: we don't need to explicitly unregister from the dirty check list. |
| // This will happen automatically at the next call to dirtyCheck. |
| if (_values != null) { |
| _mirror = null; |
| _values = null; |
| } |
| } |
| |
| bool deliverChanges() { |
| if (_values == null || !hasObservers) return false; |
| |
| // Start with manually notified records (computed properties, etc), |
| // then scan all fields for additional changes. |
| List records = _records; |
| _records = null; |
| |
| _values.forEach((name, oldValue) { |
| var newValue = _mirror.getField(name).reflectee; |
| if (!identical(oldValue, newValue)) { |
| if (records == null) records = []; |
| records.add(new PropertyChangeRecord(name)); |
| _values[name] = newValue; |
| } |
| }); |
| |
| if (records == null) return false; |
| |
| _changes.add(new UnmodifiableListView<ChangeRecord>(records)); |
| return true; |
| } |
| |
| /** |
| * Notify that the field [name] of this object has been changed. |
| * |
| * The [oldValue] and [newValue] are also recorded. If the two values are |
| * identical, no change will be recorded. |
| * |
| * For convenience this returns [newValue]. |
| */ |
| notifyPropertyChange(Symbol field, Object oldValue, Object newValue) |
| => _notifyPropertyChange(this, field, oldValue, newValue); |
| |
| /** |
| * Notify a change manually. This is *not* required for fields, but can be |
| * used for computed properties. *Note*: unlike [ChangeNotifierMixin] this |
| * will not schedule [deliverChanges]; use [Observable.dirtyCheck] instead. |
| */ |
| void notifyChange(ChangeRecord record) { |
| if (!hasObservers) return; |
| |
| if (_records == null) _records = []; |
| _records.add(record); |
| } |
| } |
| |
| /** |
| * Notify the property change. Shorthand for: |
| * |
| * target.notifyChange(new PropertyChangeRecord(targetName)); |
| */ |
| void notifyProperty(Observable target, Symbol targetName) { |
| target.notifyChange(new PropertyChangeRecord(targetName)); |
| } |
| |
| // TODO(jmesserly): remove the instance method and make this top-level method |
| // public instead? |
| _notifyPropertyChange(Observable obj, Symbol field, Object oldValue, |
| Object newValue) { |
| |
| // TODO(jmesserly): should this be == instead of identical, to prevent |
| // spurious loops? |
| if (obj.hasObservers && !identical(oldValue, newValue)) { |
| obj.notifyChange(new PropertyChangeRecord(field)); |
| } |
| return newValue; |
| } |
| |
| |
| /** |
| * The type of the `@observable` annotation. |
| * |
| * Library private because you should be able to use the [observable] field |
| * to get the one and only instance. We could make it public though, if anyone |
| * needs it for some reason. |
| */ |
| class _ObservableAnnotation { |
| const _ObservableAnnotation(); |
| } |