Skip to content

Commit 06a4b48

Browse files
committed
feat(dccd): add Support for ObservableList, ObservableMap & ChangeNotifier
Fixes dart-archive#773 Closes dart-archive#1156
1 parent e1bd4ac commit 06a4b48

File tree

4 files changed

+674
-453
lines changed

4 files changed

+674
-453
lines changed

example/pubspec.yaml

+1
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,4 @@ dependencies:
99

1010
transformers:
1111
- angular
12+

lib/change_detection/dirty_checking_change_detector.dart

+122-38
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@ library dirty_checking_change_detector;
22

33
import 'dart:collection';
44
import 'package:angular/change_detection/change_detection.dart';
5+
import 'dart:async';
6+
import 'package:observe/observe.dart' as obs;
7+
import 'package:smoke/smoke.dart' as smoke;
58

69
/**
710
* [DirtyCheckingChangeDetector] determines which object properties have changed
@@ -46,6 +49,9 @@ class DirtyCheckingChangeDetectorGroup<H> implements ChangeDetectorGroup<H> {
4649
*/
4750
DirtyCheckingRecord _recordHead, _recordTail;
4851

52+
/// Registry of record with their notifiers
53+
Map<DirtyCheckingRecord<H>, StreamSubscription> _observableRecords;
54+
4955
/**
5056
* Same as [_tail] but includes child-group records as well.
5157
*/
@@ -116,8 +122,7 @@ class DirtyCheckingChangeDetectorGroup<H> implements ChangeDetectorGroup<H> {
116122

117123
WatchRecord<H> watch(Object object, String field, H handler) {
118124
assert(_root != null); // prove that we are not deleted connected;
119-
return _recordAdd(new DirtyCheckingRecord(this, _fieldGetterFactory,
120-
handler, field, object));
125+
return _recordAdd(new DirtyCheckingRecord(this, _fieldGetterFactory, handler, field, object));
121126
}
122127

123128
/**
@@ -146,6 +151,7 @@ class DirtyCheckingChangeDetectorGroup<H> implements ChangeDetectorGroup<H> {
146151
var root;
147152
assert((root = _root) != null);
148153
assert(root._assertRecordsOk());
154+
_cancelChildObservables();
149155
DirtyCheckingRecord prevRecord = _recordHead._prevRecord;
150156
var childInclRecordTail = _childInclRecordTail;
151157
DirtyCheckingRecord nextRecord = childInclRecordTail._nextRecord;
@@ -191,6 +197,8 @@ class DirtyCheckingChangeDetectorGroup<H> implements ChangeDetectorGroup<H> {
191197
}
192198

193199
void _recordRemove(DirtyCheckingRecord record) {
200+
_cancelObservable(record);
201+
194202
DirtyCheckingRecord previous = record._prevRecord;
195203
DirtyCheckingRecord next = record._nextRecord;
196204

@@ -209,6 +217,35 @@ class DirtyCheckingChangeDetectorGroup<H> implements ChangeDetectorGroup<H> {
209217
}
210218
}
211219

220+
/// Register an observable object in this group.
221+
void _registerObservable(DirtyCheckingRecord<H> record, StreamSubscription subscription) {
222+
if (_observableRecords == null) _observableRecords = new HashMap.identity();
223+
_observableRecords[record] = subscription;
224+
}
225+
226+
/// Cancel an observable subscription in this group.
227+
void _cancelObservable(DirtyCheckingRecord<H> record) {
228+
if (_observableRecords == null) return;
229+
var subscription = _observableRecords.remove(record);
230+
if (subscription != null) subscription.cancel();
231+
}
232+
233+
/// Cancel all observable subscriptions in this group.
234+
void _cancelOwnObservables() {
235+
if (_observableRecords != null) {
236+
_observableRecords.values.forEach((s) => s.cancel());
237+
_observableRecords = null;
238+
}
239+
}
240+
241+
/// Cancel all observable subscriptions in this group and descendant groups
242+
void _cancelChildObservables() {
243+
this._cancelOwnObservables();
244+
for (var child = _childHead; child != null; child = child._next) {
245+
child._cancelChildObservables();
246+
}
247+
}
248+
212249
String toString() {
213250
var lines = [];
214251
if (_parent == null) {
@@ -306,6 +343,7 @@ class DirtyCheckingChangeDetector<H> extends DirtyCheckingChangeDetectorGroup<H>
306343
Iterator<Record<H>> collectChanges({EvalExceptionHandler exceptionHandler,
307344
AvgStopwatch stopwatch}) {
308345
if (stopwatch != null) stopwatch.start();
346+
309347
DirtyCheckingRecord changeTail = _fakeHead;
310348
DirtyCheckingRecord current = _recordHead; // current index
311349

@@ -375,18 +413,26 @@ class DirtyCheckingRecord<H> implements Record<H>, WatchRecord<H> {
375413
'NOOP',
376414
'IDENTITY',
377415
'GETTER',
378-
'GETTER / CLOSURE'
416+
'NOTIFIED GETTER',
417+
'GETTER / CLOSURE',
418+
'OBSERVABLE GETTER / CLOSURE',
379419
'MAP[]',
380420
'ITERABLE',
381-
'MAP'];
421+
'NOTIFIED LIST',
422+
'MAP',
423+
'NOTIFIED MAP'];
382424
static const int _MODE_MARKER_ = 0;
383425
static const int _MODE_NOOP_ = 1;
384426
static const int _MODE_IDENTITY_ = 2;
385427
static const int _MODE_GETTER_ = 3;
386-
static const int _MODE_GETTER_OR_METHOD_CLOSURE_ = 4;
387-
static const int _MODE_MAP_FIELD_ = 5;
388-
static const int _MODE_ITERABLE_ = 6;
389-
static const int _MODE_MAP_ = 7;
428+
static const int _MODE_GETTER_NOTIFIED_ = 4;
429+
static const int _MODE_GETTER_OR_METHOD_CLOSURE_ = 5;
430+
static const int _MODE_GETTER_OBS_OR_METHOD_CLOSURE_ = 6;
431+
static const int _MODE_MAP_FIELD_ = 7;
432+
static const int _MODE_ITERABLE_ = 8;
433+
static const int _MODE_LIST_NOTIFIED_ = 9;
434+
static const int _MODE_MAP_ = 10;
435+
static const int _MODE_MAP_NOTIFIED_ = 11;
390436

391437
final DirtyCheckingChangeDetectorGroup _group;
392438
final FieldGetterFactory _fieldGetterFactory;
@@ -403,8 +449,7 @@ class DirtyCheckingRecord<H> implements Record<H>, WatchRecord<H> {
403449
var _object;
404450
FieldGetter _getter;
405451

406-
DirtyCheckingRecord(this._group, this._fieldGetterFactory, this.handler,
407-
this.field, _object) {
452+
DirtyCheckingRecord(this._group, this._fieldGetterFactory, this.handler, this.field, _object) {
408453
object = _object;
409454
}
410455

@@ -424,7 +469,10 @@ class DirtyCheckingRecord<H> implements Record<H>, WatchRecord<H> {
424469
* reflection. If [Map] then it sets up map accessor.
425470
*/
426471
void set object(obj) {
472+
_group._cancelObservable(this);
473+
427474
_object = obj;
475+
428476
if (obj == null) {
429477
_mode = _MODE_IDENTITY_;
430478
_getter = null;
@@ -434,30 +482,41 @@ class DirtyCheckingRecord<H> implements Record<H>, WatchRecord<H> {
434482
if (field == null) {
435483
_getter = null;
436484
if (obj is Map) {
437-
if (_mode != _MODE_MAP_) {
438-
_mode = _MODE_MAP_;
485+
if (currentValue is! _MapChangeRecord) {
439486
currentValue = new _MapChangeRecord();
440-
}
441-
if (currentValue.isDirty) {
442-
// We're dirty because the mapping we tracked by reference mutated.
443-
// In addition, our reference has now changed. We should compare the
444-
// previous reported value of that mapping with the one from the
445-
// new reference.
487+
} else if (currentValue.isDirty) {
488+
// We're dirty because the mapping we tracked by reference mutated. In addition, our
489+
// reference has now changed. We should compare the previous reported value of that
490+
// mapping with the one from the new reference.
446491
currentValue._revertToPreviousState();
447492
}
448-
493+
if (obj is obs.ChangeNotifier) {
494+
_mode = _MODE_MAP_NOTIFIED_; // Run the dccd after the map is added
495+
var subscription = (obj as obs.ChangeNotifier).changes.listen((_) {
496+
_mode = _MODE_MAP_NOTIFIED_; // Run the dccd after the map is updated
497+
});
498+
_group._registerObservable(this, subscription);
499+
} else {
500+
_mode = _MODE_MAP_;
501+
}
449502
} else if (obj is Iterable) {
450-
if (_mode != _MODE_ITERABLE_) {
451-
_mode = _MODE_ITERABLE_;
503+
if (currentValue is! _CollectionChangeRecord) {
452504
currentValue = new _CollectionChangeRecord();
453-
}
454-
if (currentValue.isDirty) {
455-
// We're dirty because the collection we tracked by reference mutated.
456-
// In addition, our reference has now changed. We should compare the
457-
// previous reported value of that collection with the one from the
458-
// new reference.
505+
} else if (currentValue.isDirty) {
506+
// We're dirty because the collection we tracked by reference mutated. In addition, our
507+
// reference has now changed. We should compare the previous reported value of that
508+
// collection with the one from the new reference.
459509
currentValue._revertToPreviousState();
460510
}
511+
if (obj is obs.ObservableList) {
512+
_mode = _MODE_LIST_NOTIFIED_; // Run the dccd after the list is added
513+
var subscription = (obj as obs.ObservableList).listChanges.listen((_) {
514+
_mode = _MODE_LIST_NOTIFIED_; // Run the dccd after the list is updated
515+
});
516+
_group._registerObservable(this, subscription);
517+
} else {
518+
_mode = _MODE_ITERABLE_;
519+
}
461520
} else {
462521
_mode = _MODE_IDENTITY_;
463522
}
@@ -469,7 +528,9 @@ class DirtyCheckingRecord<H> implements Record<H>, WatchRecord<H> {
469528
_mode = _MODE_MAP_FIELD_;
470529
_getter = null;
471530
} else {
472-
_mode = _MODE_GETTER_OR_METHOD_CLOSURE_;
531+
_mode = obj is obs.ChangeNotifier ?
532+
_MODE_GETTER_OBS_OR_METHOD_CLOSURE_ :
533+
_MODE_GETTER_OR_METHOD_CLOSURE_;
473534
_getter = _fieldGetterFactory.getter(obj, field);
474535
}
475536
}
@@ -485,28 +546,55 @@ class DirtyCheckingRecord<H> implements Record<H>, WatchRecord<H> {
485546
case _MODE_GETTER_:
486547
current = _getter(object);
487548
break;
549+
case _MODE_GETTER_NOTIFIED_:
550+
_mode = _MODE_NOOP_; // no-op until next notification
551+
current = _getter(object);
552+
break;
488553
case _MODE_GETTER_OR_METHOD_CLOSURE_:
489-
// NOTE: When Dart looks up a method "foo" on object "x", it returns a
490-
// new closure for each lookup. They compare equal via "==" but are no
491-
// identical(). There's no point getting a new value each time and
492-
// decide it's the same so we'll skip further checking after the first
493-
// time.
554+
// NOTE: When Dart looks up a method "foo" on object "x", it returns a new closure for each
555+
// lookup. They compare equal via "==" but are no identical(). There's no point getting a
556+
// new value each time and decide it's the same so we'll skip further checking after the
557+
// first time.
494558
current = _getter(object);
495559
if (current is Function && !identical(current, _getter(object))) {
496560
_mode = _MODE_NOOP_;
497561
} else {
498562
_mode = _MODE_GETTER_;
499563
}
500564
break;
565+
case _MODE_GETTER_OBS_OR_METHOD_CLOSURE_:
566+
current = _getter(object);
567+
// no-op if closure (see _MODE_GETTER_OR_METHOD_CLOSURE_) or until the next notification
568+
_mode = _MODE_NOOP_;
569+
if (current is! Function || identical(current, _getter(object))) {
570+
var subscription = (object as obs.ChangeNotifier).changes.listen((records) {
571+
for (var rec in records) {
572+
// todo(vicb) Change to String if dartbug.com/17863 eventually gets fixed
573+
if (smoke.symbolToName(rec.name) == field) {
574+
// Run the dccd after the observed `object.field` is updated
575+
_mode = _MODE_GETTER_NOTIFIED_;
576+
break;
577+
}
578+
}
579+
});
580+
_group._registerObservable(this, subscription);
581+
}
582+
break;
501583
case _MODE_MAP_FIELD_:
502584
current = object[field];
503585
break;
504586
case _MODE_IDENTITY_:
505587
current = object;
506588
_mode = _MODE_NOOP_;
507589
break;
590+
case _MODE_MAP_NOTIFIED_:
591+
_mode = _MODE_NOOP_; // no-op until next notification
592+
return (currentValue as _MapChangeRecord)._check(object);
508593
case _MODE_MAP_:
509594
return (currentValue as _MapChangeRecord)._check(object);
595+
case _MODE_LIST_NOTIFIED_:
596+
_mode = _MODE_NOOP_; // no-op until next notification
597+
return (currentValue as _CollectionChangeRecord)._check(object);
510598
case _MODE_ITERABLE_:
511599
return (currentValue as _CollectionChangeRecord)._check(object);
512600
default:
@@ -515,8 +603,7 @@ class DirtyCheckingRecord<H> implements Record<H>, WatchRecord<H> {
515603

516604
var last = currentValue;
517605
if (!identical(last, current)) {
518-
if (last is String && current is String &&
519-
last == current) {
606+
if (last is String && current is String && last == current) {
520607
// This is false change in strings we need to recover, and pretend it
521608
// is the same. We save the value so that next time identity will pass
522609
currentValue = current;
@@ -531,7 +618,6 @@ class DirtyCheckingRecord<H> implements Record<H>, WatchRecord<H> {
531618
return false;
532619
}
533620

534-
535621
void remove() {
536622
_group._recordRemove(this);
537623
}
@@ -540,8 +626,6 @@ class DirtyCheckingRecord<H> implements Record<H>, WatchRecord<H> {
540626
'${_mode < _MODE_NAMES.length ? _MODE_NAMES[_mode] : '?'}[$field]{$hashCode}';
541627
}
542628

543-
final Object _INITIAL_ = new Object();
544-
545629
class _MapChangeRecord<K, V> implements MapChangeRecord<K, V> {
546630
final _records = new HashMap<dynamic, KeyValueRecord>();
547631
Map _map;

pubspec.yaml

+1
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ dependencies:
2222
di: '>=2.0.0-alpha.10 <3.0.0'
2323
html5lib: '>=0.10.0 <0.11.0'
2424
intl: '>=0.8.7 <0.10.0'
25+
observe: '>=0.10.0+1 <0.11.0'
2526
perf_api: '>=0.0.8 <0.1.0'
2627
route_hierarchical: '>=0.4.21 <0.5.0'
2728
web_components: '>=0.3.3 <0.4.0'

0 commit comments

Comments
 (0)