3

I am using the observe package.

Consider this example:

class Product extends Object with ChangeNotifier {

  double _price = 0.0;

  @reflectable double get price => _price;
  @reflectable void set price(double value) {
    if (value == null) throw new ArgumentError();
    _price = notifyPropertyChange(#price, price, value);
  }
}

class Order extends Object with ChangeNotifier {

  final ObservableList<Product> products = new ObservableList<Product>();

  double get total {
    double sum = 0.0;
    for (var item in products) {
      sum += item.price;
    }
    return sum;
  }
}

// Synchronizes the view total with the order total.
// Or rather, I'd like it to do that.
var order = new Order();
order.changes.listen((records) {
  view.total = order.total;
});

How would I rewrite this example to make it work?

I would like to be notified of any changes to the object's state, even if they happen to the list or the items of the list.

Do I have to manage change subscriptions to all items and the list itself? Inside or outside of the Order class? Through which property would I notify the change? It seems messy either way.

Günter Zöchbauer
  • 623,577
  • 216
  • 2,003
  • 1,567
Lucius
  • 3,705
  • 2
  • 22
  • 41

2 Answers2

2

The elements in the ObservableList do not propagate the notification to the list that contains them. They can't because they have no reference to the list. Also the list does not forward the notifications to the class it is referenced by.

Not really satisfying but the best I could come up with.

import 'dart:async' as async;
import 'package:observe/observe.dart';

class Product extends Object with ChangeNotifier {

  double _price = 0.0;

  @reflectable double get price => _price;
  @reflectable void set price(double value) {
    if (value == null) throw new ArgumentError();
    _price = notifyPropertyChange(#price, price, value);
  }

  @override
  String toString() => 'Product - price: $price';
}

class Order extends Object with ChangeNotifier {

  final ObservableList<Product> products = new ObservableList<Product>();

  // keep listeners to be able to cancel them
  final List<async.StreamSubscription> subscriptions = [];

  Order() {
    products.changes.listen((cr) {
      // only react to length changes (isEmpty, isNotempty changes are redundant)
      var lengthChanges = cr.where((c) => c.name == #length);
      if(lengthChanges.isNotEmpty) {
        lengthChanges.forEach((lc) =>
        notifyChange(lc));
        // we can't know if only additions/removals were done therefore we
        // cancel all existing listeners and set up new ones for all items
        // after each length change
        _updateProductsListeners();
      }
    });
    // initial setup
    _updateProductsListeners();
  }

  // cancel all product change listeners and create new ones 
  void _updateProductsListeners() {
    print('updateListeners');
    subscriptions.forEach((s) => s.cancel());
    subscriptions.clear();
    products.forEach((p)
    => subscriptions.add(p.changes.listen((crs) =>
    crs.forEach((cr) =>
      notifyPropertyChange(cr.name, cr.oldValue, cr.newValue)))));
  }

  double get total {
    double sum = 0.0;
    for (var item in products) {
      sum += item.price;
    }
    return sum;
  }
}

void main() {
// Synchronizes the view total with the order total.
// Or rather, I'd like it to do that.
  var order = new Order();
  order.changes.listen((records) {
    //view.total = order.total;
    records.forEach(print);
  });

  // a PathObserver example but it doesn't seem to be more convenient
  var op = new PathObserver(order, 'products[3].price')..open((c) =>
    print(c));

  var prods = [new Product()..price = 1.0, new Product()..price = 2.0, new Product()..price = 3.0, new Product()..price= 4.0];
  var prods2 = [new Product()..price = 5.0, new Product()..price = 6.0];

  order.products.addAll(prods);

  // use Future to allow change notification propagate between changes
  new async.Future(() =>
  order.products..addAll(prods2)..removeWhere((p) => p.price < 3.0))
  .then((_) => new async.Future(() => order.products[3].price = 7.0));

  new async.Future.delayed(new Duration(seconds: 1), () => print('done'));
}

I suggest to use something like an event bus for this where the objects that want/should to notify about something just send and event and objects that are interested in something listen for that without any knowledge of where the other object exists.

For example https://pub.dartlang.org/packages/event_bus

Günter Zöchbauer
  • 623,577
  • 216
  • 2,003
  • 1,567
  • 2
    I wrote a [`TrulyObservableList`](http://stackoverflow.com/q/1427471/925580) with your approach, sending `ListElementChangeRecords` through an additional `listElementChanges` stream. I decided to discard `package:observe` for now and go with my own slow `ChangeNotifier` that does only indicate that the object *did* change, but not *what* has changed. This solves issues with computed getters as well. Danke sehr! – Lucius Sep 29 '14 at 09:03
0

Another solution is to use the ListPathObserver. The class is deprecated but you can copy his code and reuse it. With that class you can listen for specific changes in the contained items. The field to watch is specified by path.

Fedy2
  • 3,147
  • 4
  • 26
  • 44