Deprecate Observable, add ChangeNotifier, setup travis (#11)
* Deprecate Observable, setup Travis
* Make changes suggested via review
* Update .travis.yml
Remove stable branch, as `collection` dependency won't resolve on it.
diff --git a/.travis.yml b/.travis.yml
new file mode 100644
index 0000000..4fb40c9
--- /dev/null
+++ b/.travis.yml
@@ -0,0 +1,8 @@
+language: dart
+
+dart:
+ - dev
+ # Currently requires a dev-branch in order to use properly.
+ # - stable
+
+script: ./tool/presubmit.sh
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 398c7c4..0c8e36d 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,36 @@
+## 0.17.0
+
+This is a larger change with a goal of no runtime changes for current
+customers, but in the future `Observable` will [become][issue_10] a very
+lightweight interface, i.e.:
+
+```dart
+abstract class Observable<C extends ChangeRecord> {
+ Stream<List<C>> get changes;
+}
+```
+
+[issue_10]: https://github.com/dart-lang/observable/issues/10
+
+* Started deprecating the wide `Observable` interface
+ * `ChangeNotifier` should be used as a base class for these methods:
+ * `Observable.observed`
+ * `Observable.unobserved`
+ * `Observable.hasObservers`
+ * `Observable.deliverChanges`
+ * `Observable.notifyChange`
+ * `PropertyChangeNotifier` should be used for these methods:
+ * `Observable.notifyPropertyChange`
+ * Temporarily, `Observable` _uses_ `ChangeNotifier`
+ * Existing users of anything but `implements Observable` should
+ move to implementing or extending `ChangeNotifier`. In a
+ future release `Observable` will reduce API surface down to
+ an abstract `Stream<List<C>> get changes`.
+* Added the `ChangeNotifier` and `PropertyChangeNotifier` classes
+ * Can be used to implement `Observable` in a generic manner
+* Observable is now `Observable<C extends ChangeRecord>`
+ * When passing a generic type `C`, `notifyPropertyChange` is illegal
+
## 0.16.0
* Refactored `MapChangeRecord`
diff --git a/lib/observable.dart b/lib/observable.dart
index 32a6560..516ff70 100644
--- a/lib/observable.dart
+++ b/lib/observable.dart
@@ -4,10 +4,11 @@
library observable;
+export 'src/change_notifier.dart' show ChangeNotifier, PropertyChangeNotifier;
export 'src/differs.dart' show Differ, EqualityDiffer, ListDiffer, MapDiffer;
-export 'src/records.dart' show ChangeRecord, ListChangeRecord, MapChangeRecord;
+export 'src/records.dart'
+ show ChangeRecord, ListChangeRecord, MapChangeRecord, PropertyChangeRecord;
export 'src/observable.dart';
export 'src/observable_list.dart';
export 'src/observable_map.dart';
-export 'src/property_change_record.dart';
export 'src/to_observable.dart';
diff --git a/lib/src/change_notifier.dart b/lib/src/change_notifier.dart
new file mode 100644
index 0000000..d86a4cd
--- /dev/null
+++ b/lib/src/change_notifier.dart
@@ -0,0 +1,144 @@
+import 'dart:async';
+
+import 'package:meta/meta.dart';
+
+import 'internal.dart';
+import 'observable.dart';
+import 'records.dart';
+
+/// Supplies [changes] and various hooks to implement [Observable].
+///
+/// May use [notifyChange] to queue a change record; they are asynchronously
+/// delivered at the end of the VM turn.
+///
+/// [ChangeNotifier] may be extended, mixed in, or used as a delegate.
+class ChangeNotifier<C extends ChangeRecord> implements Observable<C> {
+ StreamController<List<C>> _changes;
+
+ bool _scheduled = false;
+ List<C> _queue;
+
+ /// Emits a list of changes when the state of the object changes.
+ ///
+ /// Changes should produced in order, if significant.
+ @override
+ Stream<List<C>> get changes {
+ return (_changes ??= new StreamController<List<C>>.broadcast(
+ sync: true,
+ onListen: observed,
+ onCancel: unobserved,
+ ))
+ .stream;
+ }
+
+ /// May override to be notified when [changes] is first observed.
+ @override
+ @protected
+ @mustCallSuper
+ void observed() {}
+
+ /// May override to be notified when [changes] is no longer observed.
+ @override
+ @protected
+ @mustCallSuper
+ void unobserved() {
+ _changes = _queue = null;
+ }
+
+ /// If [hasObservers], synchronously emits [changes] that have been queued.
+ ///
+ /// Returns `true` if changes were emitted.
+ @override
+ @protected
+ @mustCallSuper
+ bool deliverChanges() {
+ List<ChangeRecord> changes;
+ if (_scheduled && hasObservers) {
+ if (_queue != null) {
+ changes = freezeInDevMode(_queue);
+ _queue = null;
+ } else {
+ changes = ChangeRecord.ANY;
+ }
+ _scheduled = false;
+ _changes.add(changes);
+ }
+ return changes != null;
+ }
+
+ /// Whether [changes] has at least one active listener.
+ ///
+ /// May be used to optimize whether to produce change records.
+ @override
+ bool get hasObservers => _changes?.hasListener == true;
+
+ /// Schedules [change] to be delivered.
+ ///
+ /// If [change] is omitted then [ChangeRecord.ANY] will be sent.
+ ///
+ /// If there are no listeners to [changes], this method does nothing.
+ @override
+ void notifyChange([C change]) {
+ if (!hasObservers) {
+ return;
+ }
+ if (change != null) {
+ (_queue ??= <C>[]).add(change);
+ }
+ if (!_scheduled) {
+ scheduleMicrotask(deliverChanges);
+ _scheduled = true;
+ }
+ }
+
+ @Deprecated('Exists to make migrations off Observable easier')
+ @override
+ @protected
+ /*=T*/ notifyPropertyChange/*<T>*/(
+ Symbol field,
+ /*=T*/
+ oldValue,
+ /*=T*/
+ newValue,
+ ) {
+ throw new UnsupportedError('Not supported by ChangeNotifier');
+ }
+}
+
+/// Implements [notifyPropertyChange] for classes that need support.
+///
+/// Will be folded entirely into [PropertyChangeNotifier] in the future.
+@Deprecated('Exists to make migrations off Observable easier')
+abstract class PropertyChangeMixin implements ChangeNotifier {
+ @override
+ /*=T*/ notifyPropertyChange/*<T>*/(
+ Symbol field,
+ /*=T*/
+ oldValue,
+ /*=T*/
+ newValue,
+ ) {
+ if (hasObservers && oldValue != newValue) {
+ notifyChange(
+ new PropertyChangeRecord/*<T>*/(
+ this,
+ field,
+ oldValue,
+ newValue,
+ ),
+ );
+ }
+ return newValue;
+ }
+}
+
+/// Supplies property `changes` and various hooks to implement [Observable].
+///
+/// May use `notifyChange` or `notifyPropertyChange` to queue a property change
+/// record; they are asynchronously delivered at the end of the VM turn.
+///
+/// [PropertyChangeNotifier] may be extended or used as a delegate. To use as
+/// a mixin, instead use with [PropertyChangeMixin]:
+/// with ChangeNotifier<PropertyChangeRecord>, PropertyChangeMixin
+class PropertyChangeNotifier = ChangeNotifier<PropertyChangeRecord>
+ with PropertyChangeMixin;
diff --git a/lib/src/differs.dart b/lib/src/differs.dart
index 8d155ed..40f005e 100644
--- a/lib/src/differs.dart
+++ b/lib/src/differs.dart
@@ -17,7 +17,7 @@
/// Generic comparisons between two comparable objects.
abstract class Differ<E> {
- /// Returns a list of change records between [e1] and [e2].
+ /// Returns a list of change records between [oldValue] and [newValue].
///
/// A return value of an empty [ChangeRecord.NONE] means no changes found.
List<ChangeRecord> diff(E oldValue, E newValue);
diff --git a/lib/src/differs/list_differ.dart b/lib/src/differs/list_differ.dart
index f55ff4f..58004a7 100644
--- a/lib/src/differs/list_differ.dart
+++ b/lib/src/differs/list_differ.dart
@@ -210,7 +210,7 @@
}
if (currentStart == currentEnd) {
- final spliceRemoved = old.sublist(oldStart, oldStart + (oldEnd - oldStart));
+ final spliceRemoved = old.sublist(oldStart, oldEnd);
return [
new ListChangeRecord/*<E>*/ .remove(
current,
diff --git a/lib/src/observable.dart b/lib/src/observable.dart
index 8a90e09..2d2c49e 100644
--- a/lib/src/observable.dart
+++ b/lib/src/observable.dart
@@ -5,74 +5,57 @@
library observable.src.observable;
import 'dart:async';
-import 'dart:collection' show UnmodifiableListView;
import 'package:meta/meta.dart';
-import 'property_change_record.dart' show PropertyChangeRecord;
-import 'records.dart' show ChangeRecord;
+import 'change_notifier.dart';
+import 'records.dart';
-/// Represents an object with observable properties. This is used by data in
-/// model-view architectures to notify interested parties of [changes] to the
-/// object's properties (fields or getter/setter pairs).
+/// Represents an object with observable state or properties.
///
/// The interface does not require any specific technique to implement
-/// observability. You can implement it in the following ways:
-///
-/// - Deriving from this class via a mixin or base class. When a field,
-/// property, or indexable item is changed, the derived class should call
-/// [notifyPropertyChange]. See that method for an example.
-/// - Implementing this interface and providing your own implementation.
-abstract class Observable {
- StreamController<List<ChangeRecord>> _changes;
+/// observability. You may implement it in the following ways:
+/// - Extend or mixin [ChangeNotifier]
+/// - Implement the interface yourself and provide your own implementation
+abstract class Observable<C extends ChangeRecord> {
+ // To be removed when https://github.com/dart-lang/observable/issues/10
+ final ChangeNotifier<C> _delegate = new ChangeNotifier<C>();
- List<ChangeRecord> _records;
+ // Whether Observable was not given a type.
+ final bool _isNotGeneric = C == dynamic;
- /// The stream of property change records to this object, delivered
- /// asynchronously.
+ /// Emits a list of changes when the state of the object changes.
///
- /// [deliverChanges] can be called to force synchronous delivery.
- Stream<List<ChangeRecord>> get changes {
- if (_changes == null) {
- _changes = new StreamController.broadcast(
- sync: true, onListen: observed, onCancel: unobserved);
- }
- return _changes.stream;
- }
+ /// Changes should produced in order, if significant.
+ Stream<List<C>> get changes => _delegate.changes;
- /// Derived classes may override this method to be called when the [changes]
- /// are first observed.
- // TODO(tvolkert): @mustCallSuper (github.com/dart-lang/sdk/issues/27275)
+ /// May override to be notified when [changes] is first observed.
@protected
- void observed() {}
+ @mustCallSuper
+ @Deprecated('Use ChangeNotifier instead to have this method available')
+ // REMOVE IGNORE when https://github.com/dart-lang/observable/issues/10
+ // ignore: invalid_use_of_protected_member
+ void observed() => _delegate.observed();
- /// Derived classes may override this method to be called when the [changes]
- /// are no longer being observed.
- // TODO(tvolkert): @mustCallSuper (github.com/dart-lang/sdk/issues/27275)
+ /// May override to be notified when [changes] is no longer observed.
@protected
- void unobserved() {
- // Free some memory
- _changes = null;
- }
+ @mustCallSuper
+ @Deprecated('Use ChangeNotifier instead to have this method available')
+ // REMOVE IGNORE when https://github.com/dart-lang/observable/issues/10
+ // ignore: invalid_use_of_protected_member
+ void unobserved() => _delegate.unobserved();
/// True if this object has any observers.
- bool get hasObservers => _changes != null && _changes.hasListener;
+ @Deprecated('Use ChangeNotifier instead to have this method available')
+ bool get hasObservers => _delegate.hasObservers;
- /// Synchronously deliver pending [changes].
+ /// If [hasObservers], synchronously emits [changes] that have been queued.
///
- /// Returns `true` if any records were delivered, otherwise `false`.
- /// Pending records will be cleared regardless, to keep newly added
- /// observers from being notified of changes that occurred before
- /// they started observing.
- bool deliverChanges() {
- List<ChangeRecord> records = _records;
- _records = null;
- if (hasObservers && records != null) {
- _changes.add(new UnmodifiableListView<ChangeRecord>(records));
- return true;
- }
- return false;
- }
+ /// Returns `true` if changes were emitted.
+ @Deprecated('Use ChangeNotifier instead to have this method available')
+ // REMOVE IGNORE when https://github.com/dart-lang/observable/issues/10
+ // ignore: invalid_use_of_protected_member
+ bool deliverChanges() => _delegate.deliverChanges();
/// Notify that the [field] name of this object has been changed.
///
@@ -80,27 +63,45 @@
/// equal, no change will be recorded.
///
/// For convenience this returns [newValue].
+ ///
+ /// ## Deprecated
+ ///
+ /// All [Observable] objects will no longer be required to emit change records
+ /// when any property changes. For example, `ObservableList` will only emit
+ /// on `ObservableList.changes`, instead of on `ObservableList.listChanges`.
+ ///
+ /// If you are using a typed `implements/extends Observable<C>`, it is illegal
+ /// to call this method - will throw an [UnsupportedError] when called.
+ @Deprecated('Use PropertyChangeNotifier')
/*=T*/ notifyPropertyChange/*<T>*/(
- Symbol field, /*=T*/ oldValue, /*=T*/ newValue) {
+ Symbol field,
+ /*=T*/
+ oldValue,
+ /*=T*/
+ newValue,
+ ) {
if (hasObservers && oldValue != newValue) {
- notifyChange(new PropertyChangeRecord(this, field, oldValue, newValue));
+ if (_isNotGeneric) {
+ notifyChange(
+ new PropertyChangeRecord(
+ this,
+ field,
+ oldValue,
+ newValue,
+ ) as C,
+ );
+ } else {
+ throw new UnsupportedError('Generic typed Observable does not support');
+ }
}
return newValue;
}
- /// Notify observers of a change.
+ /// Schedules [change] to be delivered.
///
- /// This will automatically schedule [deliverChanges].
+ /// If [change] is omitted then [ChangeRecord.ANY] will be sent.
///
- /// For most objects [Observable.notifyPropertyChange] is more convenient, but
- /// collections sometimes deliver other types of changes such as a
- /// [MapChangeRecord].
- void notifyChange(ChangeRecord record) {
- if (!hasObservers) return;
- if (_records == null) {
- _records = [];
- scheduleMicrotask(deliverChanges);
- }
- _records.add(record);
- }
+ /// If there are no listeners to [changes], this method does nothing.
+ @Deprecated('Use ChangeNotifier instead to have this method available')
+ void notifyChange([C change]) => _delegate.notifyChange(change);
}
diff --git a/lib/src/observable_map.dart b/lib/src/observable_map.dart
index 54e40e2..caebbda 100644
--- a/lib/src/observable_map.dart
+++ b/lib/src/observable_map.dart
@@ -6,9 +6,8 @@
import 'dart:collection';
-import 'observable.dart' show Observable;
-import 'property_change_record.dart' show PropertyChangeRecord;
-import 'records.dart' show MapChangeRecord;
+import 'observable.dart';
+import 'records.dart';
import 'to_observable.dart';
// TODO(jmesserly): this needs to be faster. We currently require multiple
diff --git a/lib/src/property_change_record.dart b/lib/src/property_change_record.dart
deleted file mode 100644
index 153cb5d..0000000
--- a/lib/src/property_change_record.dart
+++ /dev/null
@@ -1,29 +0,0 @@
-// Copyright (c) 2016, 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.
-
-library observable.src.property_change_record;
-
-import 'observable.dart';
-import 'records.dart' show ChangeRecord;
-
-/// A change record to a field of an [Observable] object.
-class PropertyChangeRecord<T> extends ChangeRecord {
- /// The object that changed.
- final Object object;
-
- /// The name of the property that changed.
- final Symbol name;
-
- /// The previous value of the property.
- final T oldValue;
-
- /// The new value of the property.
- final T newValue;
-
- PropertyChangeRecord(this.object, this.name, this.oldValue, this.newValue);
-
- @override
- String toString() =>
- '#<PropertyChangeRecord $name from: $oldValue to: $newValue>';
-}
diff --git a/lib/src/records.dart b/lib/src/records.dart
index 07a9826..87d7d4f 100644
--- a/lib/src/records.dart
+++ b/lib/src/records.dart
@@ -11,6 +11,7 @@
part 'records/list_change_record.dart';
part 'records/map_change_record.dart';
+part 'records/property_change_record.dart';
/// Result of a change to an observed object.
class ChangeRecord {
diff --git a/lib/src/records/map_change_record.dart b/lib/src/records/map_change_record.dart
index 9150732..cd37fc9 100644
--- a/lib/src/records/map_change_record.dart
+++ b/lib/src/records/map_change_record.dart
@@ -55,10 +55,10 @@
bool operator ==(Object o) {
if (o is MapChangeRecord<K, V>) {
return key == o.key &&
- oldValue == o.oldValue &&
- newValue == o.newValue &&
- isInsert == o.isInsert &&
- isRemove == o.isRemove;
+ oldValue == o.oldValue &&
+ newValue == o.newValue &&
+ isInsert == o.isInsert &&
+ isRemove == o.isRemove;
}
return false;
}
diff --git a/lib/src/records/property_change_record.dart b/lib/src/records/property_change_record.dart
new file mode 100644
index 0000000..74566d5
--- /dev/null
+++ b/lib/src/records/property_change_record.dart
@@ -0,0 +1,31 @@
+// Copyright (c) 2016, 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 observable.src.records;
+
+/// A change record to a field of a generic observable object.
+class PropertyChangeRecord<T> implements ChangeRecord {
+ /// Object that changed.
+ final Object object;
+
+ /// Name of the property that changed.
+ final Symbol name;
+
+ /// Previous value of the property.
+ final T oldValue;
+
+ /// New value of the property.
+ final T newValue;
+
+ const PropertyChangeRecord(
+ this.object,
+ this.name,
+ this.oldValue,
+ this.newValue,
+ );
+
+ @override
+ String toString() => ''
+ '#<$PropertyChangeRecord $name from $oldValue to: $newValue';
+}
diff --git a/pubspec.yaml b/pubspec.yaml
index a5451f0..9d1baed 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -1,5 +1,5 @@
name: observable
-version: 0.16.0
+version: 0.17.0
author: Dart Team <misc@dartlang.org>
description: Support for marking objects as observable
homepage: https://github.com/dart-lang/observable
@@ -10,4 +10,5 @@
meta: '^1.0.4'
quiver: '^0.24.0'
dev_dependencies:
+ dart_style: '^0.2.0'
test: '^0.12.0'
diff --git a/test/observable_test.dart b/test/observable_test.dart
index eb824c7..af4d9c3 100644
--- a/test/observable_test.dart
+++ b/test/observable_test.dart
@@ -206,7 +206,7 @@
createModel(int number) => new ObservableSubclass(number);
-class ObservableSubclass<T> extends Observable {
+class ObservableSubclass<T> extends PropertyChangeNotifier {
ObservableSubclass([T initialValue]) : _value = initialValue;
T get value => _value;
diff --git a/tool/presubmit.sh b/tool/presubmit.sh
new file mode 100755
index 0000000..364bee7
--- /dev/null
+++ b/tool/presubmit.sh
@@ -0,0 +1,29 @@
+#!/bin/bash
+
+# Make sure dartfmt is run on everything
+# This assumes you have dart_style as a dev_dependency
+echo "Checking dartfmt..."
+NEEDS_DARTFMT="$(find lib test -name "*.dart" | xargs pub run dart_style:format -n)"
+if [[ ${NEEDS_DARTFMT} != "" ]]
+then
+ echo "FAILED"
+ echo "${NEEDS_DARTFMT}"
+ exit 1
+fi
+echo "PASSED"
+
+# Make sure we pass the analyzer
+echo "Checking dartanalyzer..."
+FAILS_ANALYZER="$(find lib test -name "*.dart" | xargs dartanalyzer --options .analysis_options)"
+if [[ $FAILS_ANALYZER == *"[error]"* ]]
+then
+ echo "FAILED"
+ echo "${FAILS_ANALYZER}"
+ exit 1
+fi
+echo "PASSED"
+
+# Fail on anything that fails going forward.
+set -e
+
+pub run test