diff --git a/README.md b/README.md index 325937de0..75efba3fd 100644 --- a/README.md +++ b/README.md @@ -433,7 +433,7 @@ liveQuery.client.unSubscribe(subscription); ## ParseLiveList ParseLiveList makes implementing a dynamic List as simple as possible. - +#### General Use It ships with the ParseLiveList class itself, this class manages all elements of the list, sorts them, keeps itself up to date and Notifies you on changes. @@ -487,8 +487,19 @@ ParseLiveListWidget( duration: Duration(seconds: 1), ); ``` +### included Sub-Objects +By default, ParseLiveQuery will provide you with all the objects you included in your Query like this: +```dart +queryBuilder.includeObject(/*List of all the included sub-objects*/); +``` +ParseLiveList will not listen for updates on this objects by default. +To activate listening for updates on all included objects, add `listenOnAllSubItems: true` to your ParseLiveListWidgets constructor. +If you want ParseLiveList to listen for updates on only some sub-objects, use `listeningIncludes: const [/*all the included sub-objects*/]` instead. +Just as QueryBuilder, ParseLiveList supports nested sub-objects too. + +**NOTE:** Currently ParseLiveList wont update your sub-objects after your client reconnects to the web. -Note: To use this features you have to enable [Live Queries](#live-queries) first. +**NOTE:** To use this features you have to enable [Live Queries](#live-queries) first. ## Users You can create and control users just as normal using this SDK. diff --git a/lib/src/utils/parse_live_list.dart b/lib/src/utils/parse_live_list.dart index ecdd8663f..a5605f354 100644 --- a/lib/src/utils/parse_live_list.dart +++ b/lib/src/utils/parse_live_list.dart @@ -6,11 +6,19 @@ import '../../parse_server_sdk.dart'; // ignore_for_file: invalid_use_of_protected_member class ParseLiveList { - ParseLiveList._(this._query); + ParseLiveList._(this._query, this._listeningIncludes); static Future> create( - QueryBuilder _query) { - final ParseLiveList parseLiveList = ParseLiveList._(_query); + QueryBuilder _query, + {bool listenOnAllSubItems, + List listeningIncludes}) { + final ParseLiveList parseLiveList = ParseLiveList._( + _query, + listenOnAllSubItems == true + ? _toIncludeMap( + _query.limiters['include']?.toString()?.split(',') ?? + []) + : _toIncludeMap(listeningIncludes ?? [])); return parseLiveList._init().then((_) { return parseLiveList; @@ -78,6 +86,8 @@ class ParseLiveList { int get nextID => _nextID++; final QueryBuilder _query; + //The included Items, where LiveList should look for updates. + final Map _listeningIncludes; int get size { return _list.length; @@ -87,6 +97,10 @@ class ParseLiveList { _query.limiters['include']?.toString()?.split(',') ?? []; Map get _includePaths { + return _toIncludeMap(includes); + } + + static Map _toIncludeMap(List includes) { final Map includesMap = {}; for (String includeString in includes) { @@ -104,6 +118,7 @@ class ParseLiveList { Stream> get stream => _eventStreamController.stream; Subscription _liveQuerySubscription; StreamSubscription _liveQueryClientEventSubscription; + final Future _updateQueue = Future.value(); Future _runQuery() async { final QueryBuilder query = QueryBuilder.copy(_query); @@ -128,8 +143,9 @@ class ParseLiveList { final ParseResponse parseResponse = await _runQuery(); if (parseResponse.success) { _list = parseResponse.results - ?.map>( - (dynamic element) => ParseLiveListElement(element)) + ?.map>((dynamic element) => + ParseLiveListElement(element, + updatedSubItems: _listeningIncludes)) ?.toList() ?? List>(); } @@ -140,11 +156,29 @@ class ParseLiveList { copyObject: _query.object.clone(_query.object.toJson())) .then((Subscription subscription) { _liveQuerySubscription = subscription; - subscription.on(LiveQueryEvent.create, _objectAdded); - subscription.on(LiveQueryEvent.update, _objectUpdated); - subscription.on(LiveQueryEvent.enter, _objectAdded); - subscription.on(LiveQueryEvent.leave, _objectDeleted); - subscription.on(LiveQueryEvent.delete, _objectDeleted); + + //This should synchronize the events. Not sure if it is necessary, but it should help preventing unexpected results. + subscription.on(LiveQueryEvent.create, + (T object) => _updateQueue.whenComplete(() => _objectAdded(object))); + subscription.on( + LiveQueryEvent.update, + (T object) => + _updateQueue.whenComplete(() => _objectUpdated(object))); + subscription.on(LiveQueryEvent.enter, + (T object) => _updateQueue.whenComplete(() => _objectAdded(object))); + subscription.on( + LiveQueryEvent.leave, + (T object) => + _updateQueue.whenComplete(() => _objectDeleted(object))); + subscription.on( + LiveQueryEvent.delete, + (T object) => + _updateQueue.whenComplete(() => _objectDeleted(object))); +// subscription.on(LiveQueryEvent.create, _objectAdded); +// subscription.on(LiveQueryEvent.update, _objectUpdated); +// subscription.on(LiveQueryEvent.enter, _objectAdded); +// subscription.on(LiveQueryEvent.leave, _objectDeleted); +// subscription.on(LiveQueryEvent.delete, _objectDeleted); }); _liveQueryClientEventSubscription = LiveQuery() @@ -198,10 +232,9 @@ class ParseLiveList { }); } - Future _loadIncludes(ParseObject object, + static Future _loadIncludes(ParseObject object, {ParseObject oldObject, Map paths}) async { - paths ??= _includePaths; - if (object == null || paths.isEmpty) return; + if (object == null || paths == null || paths.isEmpty) return; final List> loadingNodes = >[]; @@ -217,6 +250,7 @@ class ParseLiveList { if (!includedObject.containsKey(keyVarUpdatedAt) || includedObject.objectId != object.get(key).objectId) { + includedObject = object.get(key); //fetch from web including sub objects //same as down there final QueryBuilder queryBuilder = QueryBuilder< @@ -268,7 +302,7 @@ class ParseLiveList { await Future.wait(loadingNodes); } - List _toIncludeStringList(Map includes) { + static List _toIncludeStringList(Map includes) { final List includeList = []; for (String key in includes.keys) { includeList.add(key); @@ -283,16 +317,24 @@ class ParseLiveList { Future _objectAdded(T object, {bool loaded = true, bool fetchedIncludes = false}) async { - if (!fetchedIncludes) await _loadIncludes(object); + //This line seems unnecessary, but without this, weird things happen. + //(Hide first element, hide second, view first, view second => second is displayed twice) + object = object?.clone(object?.toJson(full: true)); + + if (!fetchedIncludes) await _loadIncludes(object, paths: _includePaths); for (int i = 0; i < _list.length; i++) { if (after(object, _list[i].object) != true) { - _list.insert(i, ParseLiveListElement(object, loaded: loaded)); + _list.insert( + i, + ParseLiveListElement(object, + loaded: loaded, updatedSubItems: _listeningIncludes)); _eventStreamController.sink.add(ParseLiveListAddEvent( i, object?.clone(object?.toJson(full: true)))); return; } } - _list.add(ParseLiveListElement(object, loaded: loaded)); + _list.add(ParseLiveListElement(object, + loaded: loaded, updatedSubItems: _listeningIncludes)); _eventStreamController.sink.add(ParseLiveListAddEvent( _list.length - 1, object?.clone(object?.toJson(full: true)))); } @@ -301,7 +343,8 @@ class ParseLiveList { for (int i = 0; i < _list.length; i++) { if (_list[i].object.get(keyVarObjectId) == object.get(keyVarObjectId)) { - await _loadIncludes(object, oldObject: _list[i].object); + await _loadIncludes(object, + oldObject: _list[i].object, paths: _includePaths); if (after(_list[i].object, object) == null) { _list[i].object = object?.clone(object?.toJson(full: true)); } else { @@ -316,10 +359,12 @@ class ParseLiveList { } } - void _objectDeleted(T object) { + Future _objectDeleted(T object) async { for (int i = 0; i < _list.length; i++) { if (_list[i].object.get(keyVarObjectId) == object.get(keyVarObjectId)) { + await _loadIncludes(object, + oldObject: _list[i].object, paths: _includePaths); _list.removeAt(i).dispose(); _eventStreamController.sink.add(ParseLiveListDeleteEvent( i, object?.clone(object?.toJson(full: true)))); @@ -389,24 +434,122 @@ class ParseLiveList { } class ParseLiveListElement { - ParseLiveListElement(this._object, {bool loaded = false}) { + ParseLiveListElement(this._object, + {bool loaded = false, Map updatedSubItems}) { if (_object != null) { _loaded = loaded; } + _updatedSubItems = + _toSubscriptionMap(updatedSubItems ?? Map()); + if (_updatedSubItems.isNotEmpty) { + _liveQuery = LiveQuery(); + _subscribe(); + } } final StreamController _streamController = StreamController.broadcast(); T _object; bool _loaded = false; + Map>, dynamic> _updatedSubItems; + LiveQuery _liveQuery; + final Future _subscriptionQueue = Future.value(); Stream get stream => _streamController?.stream; // ignore: invalid_use_of_protected_member T get object => _object?.clone(_object?.toJson(full: true)); + Map>, dynamic> _toSubscriptionMap( + Map map) { + Map>, dynamic> result = + Map>, dynamic>(); + for (String key in map.keys) { + result.putIfAbsent(MapEntry>(key, null), + () => _toSubscriptionMap(map[key])); + } + return result; + } + + Map _toKeyMap( + Map>, dynamic> map) { + final Map result = Map(); + for (MapEntry> key in map.keys) { + result.putIfAbsent(key.key, () => _toKeyMap(map[key])); + } + return result; + } + + void _subscribe() { + _subscriptionQueue.whenComplete(() async { + if (_updatedSubItems.isNotEmpty && _object != null) { + final List> tasks = >[]; + for (MapEntry> key + in _updatedSubItems.keys) { + tasks.add(_subscribeSubItem(_object, key, + _object.get(key.key), _updatedSubItems[key])); + } + await Future.wait(tasks); + } + }); + } + + void _unsubscribe( + Map>, dynamic> subscriptions) { + for (MapEntry> key + in subscriptions.keys) { + if (_liveQuery != null && key.value != null) + _liveQuery.client.unSubscribe(key.value); + _unsubscribe(subscriptions[key]); + } + } + + Future _subscribeSubItem( + ParseObject parentObject, + MapEntry> currentKey, + ParseObject subObject, + Map>, dynamic> path) async { + if (_liveQuery != null && subObject != null) { + final List> tasks = >[]; + for (MapEntry> key in path.keys) { + tasks.add(_subscribeSubItem( + subObject, key, subObject.get(key.key), path[key])); + } + final QueryBuilder queryBuilder = + QueryBuilder(subObject) + ..whereEqualTo(keyVarObjectId, subObject.objectId); + + tasks.add(_liveQuery.client + .subscribe(queryBuilder) + .then((Subscription subscription) { + subscription.on(LiveQueryEvent.update, (ParseObject newObject) async { + _subscriptionQueue.whenComplete(() async { + await ParseLiveList._loadIncludes(newObject, + oldObject: subObject, paths: _toKeyMap(path)); + parentObject.getObjectData()[currentKey.key] = newObject; + if (!_streamController.isClosed) { + _streamController + ?.add(_object?.clone(_object?.toJson(full: true))); + //Resubscribe subitems + _unsubscribe(path); + for (MapEntry> key + in path.keys) { + tasks.add(_subscribeSubItem(subObject, key, + subObject.get(key.key), path[key])); + } + } + await Future.wait(tasks); + }); + }); + })); + await Future.wait(tasks); + } + } + set object(T value) { _loaded = true; _object = value; + _unsubscribe(_updatedSubItems); + _subscribe(); // ignore: invalid_use_of_protected_member _streamController?.add(_object?.clone(_object?.toJson(full: true))); } @@ -414,6 +557,7 @@ class ParseLiveListElement { bool get loaded => _loaded; void dispose() { + _unsubscribe(_updatedSubItems); _streamController.close(); } } @@ -456,21 +600,23 @@ class ParseLiveListElementSnapshot { } class ParseLiveListWidget extends StatefulWidget { - const ParseLiveListWidget( - {Key key, - @required this.query, - this.listLoadingElement, - this.duration = const Duration(milliseconds: 300), - this.scrollPhysics, - this.scrollController, - this.scrollDirection = Axis.vertical, - this.padding, - this.primary, - this.reverse = false, - this.childBuilder, - this.shrinkWrap = false, - this.removedItemBuilder}) - : super(key: key); + const ParseLiveListWidget({ + Key key, + @required this.query, + this.listLoadingElement, + this.duration = const Duration(milliseconds: 300), + this.scrollPhysics, + this.scrollController, + this.scrollDirection = Axis.vertical, + this.padding, + this.primary, + this.reverse = false, + this.childBuilder, + this.shrinkWrap = false, + this.removedItemBuilder, + this.listenOnAllSubItems, + this.listeningIncludes, + }) : super(key: key); final QueryBuilder query; final Widget listLoadingElement; @@ -487,9 +633,16 @@ class ParseLiveListWidget extends StatefulWidget { final ChildBuilder childBuilder; final ChildBuilder removedItemBuilder; + final bool listenOnAllSubItems; + final List listeningIncludes; + @override - _ParseLiveListWidgetState createState() => - _ParseLiveListWidgetState(query, removedItemBuilder); + _ParseLiveListWidgetState createState() => _ParseLiveListWidgetState( + query: query, + removedItemBuilder: removedItemBuilder, + listenOnAllSubItems: listenOnAllSubItems, + listeningIncludes: listeningIncludes, + ); static Widget defaultChildBuilder( BuildContext context, ParseLiveListElementSnapshot snapshot) { @@ -513,8 +666,16 @@ class ParseLiveListWidget extends StatefulWidget { class _ParseLiveListWidgetState extends State> { - _ParseLiveListWidgetState(this._query, this.removedItemBuilder) { - ParseLiveList.create(_query).then((ParseLiveList value) { + _ParseLiveListWidgetState( + {@required this.query, + @required this.removedItemBuilder, + bool listenOnAllSubItems, + List listeningIncludes}) { + ParseLiveList.create( + query, + listenOnAllSubItems: listenOnAllSubItems, + listeningIncludes: listeningIncludes, + ).then((ParseLiveList value) { setState(() { _liveList = value; _liveList.stream.listen((ParseLiveListEvent event) { @@ -543,7 +704,7 @@ class _ParseLiveListWidgetState }); } - final QueryBuilder _query; + final QueryBuilder query; ParseLiveList _liveList; final GlobalKey _animatedListKey = GlobalKey();