blob: d39f84ab96eebd1e6bc73f9bbf921fd5acf71a21 [file] [log] [blame]
// 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();
}