From 612404c01991ff4aa279c4a08cd89d7a27e071b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kasper=20Overg=C3=A5rd=20Nielsen?= Date: Wed, 19 Jan 2022 16:10:13 +0100 Subject: [PATCH] Add notifications to RealmObject --- lib/src/native/realm_core.dart | 42 ++++++++++++++++++++++++++++++++++ lib/src/object_changes.dart | 31 +++++++++++++++++++++++++ lib/src/realm_object.dart | 4 ++++ test/realm_test.dart | 39 +++++++++++++++++++++++++++++++ 4 files changed, 116 insertions(+) create mode 100644 lib/src/object_changes.dart diff --git a/lib/src/native/realm_core.dart b/lib/src/native/realm_core.dart index 856964f58f..9cda1591ed 100644 --- a/lib/src/native/realm_core.dart +++ b/lib/src/native/realm_core.dart @@ -27,6 +27,7 @@ import '../collection_changes.dart'; import '../configuration.dart'; import '../init.dart'; import '../list.dart'; +import '../object_changes.dart'; import '../realm_class.dart'; import '../realm_object.dart'; import '../results.dart'; @@ -391,6 +392,43 @@ class _RealmCore { }); } + int getObjectChangesCount(RealmObjectChangesHandle changes) { + return _realmLib.realm_object_changes_get_num_modified_properties(changes._pointer); + } + + List getObjectChanges(RealmObjectChangesHandle changes, int max) { + return using((arena) { + final out_modified = arena(max); + _realmLib.realm_object_changes_get_modified_properties(changes._pointer, out_modified, max); + return out_modified != nullptr ? out_modified.asTypedList(max).toList() : const []; + }); + } + + Stream realmObjectChanged(RealmObject object, SchedulerHandle scheduler) { + late StreamController controller; + + void callback(Pointer data) { + final changes = RealmObjectChanges( + RealmObjectChangesHandle._(_realmLib.realm_clone(data).cast()), + object.realm!, + ); + controller.add(changes); + } + + controller = _constructRealmNotificationStreamController( + (userData, callback, free, error) => _realmLib.realm_object_add_notification_callback( + object.handle._pointer, + userData, + free, + callback.cast(), + error, + scheduler._pointer, + ), + callback); + + return controller.stream; + } + Counts getCollectionChangesCounts(RealmCollectionChangesHandle changes) { return using((arena) { final out_num_deletions = arena(); @@ -651,6 +689,10 @@ class RealmCollectionChangesHandle extends Handle { RealmCollectionChangesHandle._(Pointer pointer) : super(pointer, 88); // TODO: What should gc hint be? } +class RealmObjectChangesHandle extends Handle { + RealmObjectChangesHandle._(Pointer pointer) : super(pointer, 88); // TODO: What should gc hint be? +} + extension _StringEx on String { Pointer toUtf8Ptr(Allocator allocator) { final units = utf8.encode(this); diff --git a/lib/src/object_changes.dart b/lib/src/object_changes.dart new file mode 100644 index 0000000000..351f260fcc --- /dev/null +++ b/lib/src/object_changes.dart @@ -0,0 +1,31 @@ +import 'realm_class.dart'; +import 'native/realm_core.dart'; + +class RealmObjectChanges { + final RealmObjectChangesHandle _handle; + final Realm realm; + + RealmObjectChanges(this._handle, this.realm); + + int? _count; + int get count => _count ??= realmCore.getObjectChangesCount(_handle); + + List? _keys; + List get keys => _keys ??= realmCore.getObjectChanges(_handle, count); +} + +class ObjectChanges extends RealmObjectChanges { + T _object; + ObjectChanges._(this._object, RealmObjectChangesHandle handle, Realm realm) : super(handle, realm); + + factory ObjectChanges(T object, RealmObjectChanges changes) { + return ObjectChanges._(object, changes._handle, changes.realm); + } + + List? _properties; + List get properties => _properties ??= keys.map((k) => realm.metadata[_object.runtimeType]!.findByKey(k)).toList(); +} + +// TODO: Perhaps let generator generate typed RealmObjectChanges subtypes, +// that can have a much nicer type safe interface + diff --git a/lib/src/realm_object.dart b/lib/src/realm_object.dart index da4c55823f..70bd2f07f9 100644 --- a/lib/src/realm_object.dart +++ b/lib/src/realm_object.dart @@ -18,6 +18,7 @@ import 'list.dart'; import 'native/realm_core.dart'; +import 'object_changes.dart'; import 'realm_class.dart'; abstract class RealmAccessor { @@ -222,6 +223,9 @@ class RealmObject { if (!isManaged || !other.isManaged) return false; return realmCore.equals(this, other); } + + // TODO: Should we generate this as a Stream> on each realm object class? + Stream get changed => realmCore.realmObjectChanged(this, realm!.scheduler.handle).map((o) => ObjectChanges(this, o)); } //RealmObject package internal members diff --git a/test/realm_test.dart b/test/realm_test.dart index 69827273ac..97ff0bef5e 100644 --- a/test/realm_test.dart +++ b/test/realm_test.dart @@ -826,6 +826,45 @@ Future main([List? args]) async { } }); + test('RealmObject.changed', () async { + var config = Configuration([Dog.schema, Person.schema]); + var realm = Realm(config); + + void write(void Function() writer) async { + realm.write(writer); + realm.write(() {}); // dummy write to raise notification from previous write + } + + final dog = Dog('Fido'); + write(() => realm.add(dog)); + + final stream = dog.changed.asBroadcastStream(); + + var callbacks = 0; + final subscription = stream.listen((_) => ++callbacks); + + { + final event = stream.skip(1).first; + write(() => dog.age = 1); + final change = await event; + expect(callbacks, 2); // first time + expect(change.count, 1); + expect(change.properties, ['age']); + } + { + final event = stream.first; + write(() { + dog.owner = Person('Kasper'); + dog.age = 2; + }); + final change = await event; + expect(callbacks, 3); // once per transaction, not once per change + expect(change.count, 2); + expect(change.properties, ['owner', 'age']); + } + subscription.cancel(); + }); + test('RealmList.changed', () async { var config = Configuration([Team.schema, Person.schema]); var realm = Realm(config);