From dd837d190ad4b880ca9317eea480d0f1ba39a917 Mon Sep 17 00:00:00 2001 From: maaeps Date: Thu, 2 Apr 2020 12:45:41 +0200 Subject: [PATCH 1/8] LiveList: reload included objects First implementation --- lib/src/utils/parse_live_list.dart | 74 +++++++++++++++++++++++++++--- 1 file changed, 68 insertions(+), 6 deletions(-) diff --git a/lib/src/utils/parse_live_list.dart b/lib/src/utils/parse_live_list.dart index 6c063ba6a..6176ade57 100644 --- a/lib/src/utils/parse_live_list.dart +++ b/lib/src/utils/parse_live_list.dart @@ -83,6 +83,30 @@ class ParseLiveList { return _list.length; } + List get includes => + _query.limiters['include']?.toString()?.split(',') ?? []; + + List> get validIncludePaths { + final List> includePaths = includes + .map>((String value) => value?.split('.')) + .toList(); + includePaths + .sort((List a, List b) => a.length.compareTo(b.length)); + return includePaths + // ignore: unnecessary_parenthesis + .where((List element) => (element.length == 1 || + includePaths.any((List otherElement) { + if (element.length - 1 == otherElement.length) { + for (int i = 0; i < element.length - 1; i++) { + if (element[i] != otherElement[i]) return false; + } + return true; + } + return false; + }))) + .toList(); + } + Stream> get stream => _eventStreamController.stream; Subscription _liveQuerySubscription; StreamSubscription _liveQueryClientEventSubscription; @@ -92,9 +116,9 @@ class ParseLiveList { if (query.limiters.containsKey('order')) { query.keysToReturn( query.limiters['order'].toString().split(',').map((String string) { - if (string.startsWith('-')) { - return string.substring(1); - } + if (string.startsWith('-')) { + return string.substring(1); + } return string; }).toList()); } else { @@ -180,7 +204,43 @@ class ParseLiveList { }); } - void _objectAdded(T object, {bool loaded = true}) { + // TODO(any): include sub-includes & use already available data. + Future _loadIncludes(T object, + {T oldObject}) async { + if (object == null) return null; + object = object.clone(object.toJson(full: true)); + final List> includes = validIncludePaths; + final List TLIncludes = includes + .where((element) => element.length == 1) + .map((e) => e[0]) + .toList(); + final Map>> loadedObjects = + >>{}; + for (String key in TLIncludes) { + if (object.containsKey(key)) { + final QueryBuilder queryBuilder = + QueryBuilder(object.get(key)); + queryBuilder.whereEqualTo( + keyVarObjectId, object.get(key).objectId); + + loadedObjects.putIfAbsent( + key, + () => queryBuilder.query().then((ParseResponse value) => + MapEntry(key, value))); + } + } + final List> responses = + await Future.wait>( + loadedObjects.values); + for (MapEntry entry in responses) { + if (entry.value.success && entry.value.results.length == 1) + object.getObjectData()[entry.key] = entry.value.results[0]; + } + return object; + } + + Future _objectAdded(T object, {bool loaded = true}) async { + object = await _loadIncludes(object); for (int i = 0; i < _list.length; i++) { if (after(object, _list[i].object) != true) { _list.insert(i, ParseLiveListElement(object, loaded: loaded)); @@ -194,7 +254,9 @@ class ParseLiveList { _list.length - 1, object?.clone(object?.toJson(full: true)))); } - void _objectUpdated(T object) { + Future _objectUpdated(T object) async { + print(object); + object = await _loadIncludes(object); for (int i = 0; i < _list.length; i++) { if (_list[i].object.get(keyVarObjectId) == object.get(keyVarObjectId)) { @@ -203,7 +265,7 @@ class ParseLiveList { } else { _list.removeAt(i).dispose(); _eventStreamController.sink.add(ParseLiveListDeleteEvent( - // ignore: invalid_use_of_protected_member + // ignore: invalid_use_of_protected_member i, object?.clone(object?.toJson(full: true)))); // ignore: invalid_use_of_protected_member From a30de875128e03496996ea05ef83291fe4d5f802 Mon Sep 17 00:00:00 2001 From: maaeps Date: Thu, 2 Apr 2020 13:05:46 +0200 Subject: [PATCH 2/8] Key was wrong on order change --- lib/src/utils/parse_live_list.dart | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/lib/src/utils/parse_live_list.dart b/lib/src/utils/parse_live_list.dart index 6176ade57..39e27cb1c 100644 --- a/lib/src/utils/parse_live_list.dart +++ b/lib/src/utils/parse_live_list.dart @@ -317,6 +317,15 @@ class ParseLiveList { return 'NotFound'; } + String getIdentifier(int index) { + if (index < _list.length) { + return _list[index].object.get(keyVarObjectId) + + _list[index].object.get(keyVarUpdatedAt)?.toString() ?? + ''; + } + return 'NotFound'; + } + T getLoadedAt(int index) { if (index < _list.length && _list[index].loaded) { return _list[index].object; @@ -521,7 +530,8 @@ class _ParseLiveListWidgetState itemBuilder: (BuildContext context, int index, Animation animation) { return ParseLiveListElementWidget( - key: ValueKey(_liveList?.idOf(index) ?? '_NotFound'), + key: ValueKey( + _liveList?.getIdentifier(index) ?? '_NotFound'), stream: () => _liveList?.getAt(index), loadedData: () => _liveList?.getLoadedAt(index), sizeFactor: animation, From 8d4a4aff902401a044219a72438f051ec838aa3b Mon Sep 17 00:00:00 2001 From: maaeps Date: Thu, 2 Apr 2020 14:41:41 +0200 Subject: [PATCH 3/8] include sub-includes --- lib/src/utils/parse_live_list.dart | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/lib/src/utils/parse_live_list.dart b/lib/src/utils/parse_live_list.dart index 39e27cb1c..3bb17902c 100644 --- a/lib/src/utils/parse_live_list.dart +++ b/lib/src/utils/parse_live_list.dart @@ -204,7 +204,7 @@ class ParseLiveList { }); } - // TODO(any): include sub-includes & use already available data. + // TODO(any): use already available data. Future _loadIncludes(T object, {T oldObject}) async { if (object == null) return null; @@ -222,7 +222,18 @@ class ParseLiveList { QueryBuilder(object.get(key)); queryBuilder.whereEqualTo( keyVarObjectId, object.get(key).objectId); - + queryBuilder.includeObject(includes + .where((List path) => path.length > 1 && path[0] == key) + .map((List path) { + String val = ''; + for (int i = 1; i < path.length; i++) { + if (i > 1) { + val += '.'; + } + val += path[i]; + } + return val; + }).toList()); loadedObjects.putIfAbsent( key, () => queryBuilder.query().then((ParseResponse value) => @@ -265,10 +276,7 @@ class ParseLiveList { } else { _list.removeAt(i).dispose(); _eventStreamController.sink.add(ParseLiveListDeleteEvent( - // ignore: invalid_use_of_protected_member - i, - object?.clone(object?.toJson(full: true)))); - // ignore: invalid_use_of_protected_member + i, object?.clone(object?.toJson(full: true)))); _objectAdded(object?.clone(object?.toJson(full: true))); } break; From f4860d22dfbc2430dcf29cd47b8564c8823f4e7a Mon Sep 17 00:00:00 2001 From: maaeps Date: Sat, 4 Apr 2020 13:07:16 +0200 Subject: [PATCH 4/8] reuse loaded pointers --- lib/src/utils/parse_live_list.dart | 224 +++++++++++++++++++++-------- 1 file changed, 162 insertions(+), 62 deletions(-) diff --git a/lib/src/utils/parse_live_list.dart b/lib/src/utils/parse_live_list.dart index 3bb17902c..c9c3506d0 100644 --- a/lib/src/utils/parse_live_list.dart +++ b/lib/src/utils/parse_live_list.dart @@ -86,25 +86,40 @@ class ParseLiveList { List get includes => _query.limiters['include']?.toString()?.split(',') ?? []; - List> get validIncludePaths { - final List> includePaths = includes - .map>((String value) => value?.split('.')) - .toList(); - includePaths - .sort((List a, List b) => a.length.compareTo(b.length)); - return includePaths - // ignore: unnecessary_parenthesis - .where((List element) => (element.length == 1 || - includePaths.any((List otherElement) { - if (element.length - 1 == otherElement.length) { - for (int i = 0; i < element.length - 1; i++) { - if (element[i] != otherElement[i]) return false; - } - return true; - } - return false; - }))) - .toList(); +// List> get validIncludePaths { +// final List> includePaths = includes +// .map>((String value) => value?.split('.')) +// .toList(); +// includePaths +// .sort((List a, List b) => a.length.compareTo(b.length)); +// return includePaths +// // ignore: unnecessary_parenthesis +// .where((List element) => (element.length == 1 || +// includePaths.any((List otherElement) { +// if (element.length - 1 == otherElement.length) { +// for (int i = 0; i < element.length - 1; i++) { +// if (element[i] != otherElement[i]) return false; +// } +// return true; +// } +// return false; +// }))) +// .toList(); +// } + + Map get _includePaths { + final Map includesMap = {}; + + for (String includeString in includes) { + final List pathParts = includeString.split('.'); + Map root = includesMap; + for (String pathPart in pathParts) { + root.putIfAbsent(pathPart, () => {}); + root = root[pathPart]; + } + } + + return includesMap; } Stream> get stream => _eventStreamController.stream; @@ -204,54 +219,138 @@ class ParseLiveList { }); } - // TODO(any): use already available data. - Future _loadIncludes(T object, - {T oldObject}) async { - if (object == null) return null; - object = object.clone(object.toJson(full: true)); - final List> includes = validIncludePaths; - final List TLIncludes = includes - .where((element) => element.length == 1) - .map((e) => e[0]) - .toList(); - final Map>> loadedObjects = - >>{}; - for (String key in TLIncludes) { + Future _loadIncludes(ParseObject object, + {ParseObject oldObject, Map paths}) async { + paths ??= _includePaths; + if (object == null || paths.isEmpty) return; + + final List> loadingNodes = >[]; + + for (String key in paths.keys) { if (object.containsKey(key)) { - final QueryBuilder queryBuilder = - QueryBuilder(object.get(key)); - queryBuilder.whereEqualTo( - keyVarObjectId, object.get(key).objectId); - queryBuilder.includeObject(includes - .where((List path) => path.length > 1 && path[0] == key) - .map((List path) { - String val = ''; - for (int i = 1; i < path.length; i++) { - if (i > 1) { - val += '.'; + ParseObject includedObject = object.get(key); + //If the object is not fetched + if (!includedObject.containsKey(keyVarUpdatedAt)) { + //See if oldObject contains key + if (oldObject != null && oldObject.containsKey(key)) { + includedObject = oldObject.get(key); + //If the object is not fetched || the ids don't match / the pointer changed + if (!includedObject.containsKey(keyVarUpdatedAt) || + includedObject.objectId != + object.get(key).objectId) { + print('fetch1 $key'); + //fetch from web including sub objects + //same as down there + final QueryBuilder queryBuilder = QueryBuilder< + ParseObject>(ParseObject(includedObject.parseClassName)) + ..whereEqualTo(keyVarObjectId, includedObject.objectId) + ..includeObject(_toIncludeStringList(paths[key])); + loadingNodes.add(queryBuilder + .query() + .then((ParseResponse parseResponse) { + print('fetched1 $key'); + if (parseResponse.success && + parseResponse.results.length == 1) { + object.getObjectData()[key] = parseResponse.results[0]; + } + })); + continue; + } else { + print('recycled $key'); + object.getObjectData()[key] = includedObject; + //recursion + loadingNodes + .add(_loadIncludes(includedObject, paths: paths[key])); + continue; } - val += path[i]; + } else { + print('fetch2 $key'); + //fetch from web including sub objects + //same as up there + final QueryBuilder queryBuilder = QueryBuilder< + ParseObject>(ParseObject(includedObject.parseClassName)) + ..whereEqualTo(keyVarObjectId, includedObject.objectId) + ..includeObject(_toIncludeStringList(paths[key])); + loadingNodes.add( + queryBuilder.query().then((ParseResponse parseResponse) { + print('fetched2 $key'); + if (parseResponse.success && parseResponse.results.length == 1) { + object.getObjectData()[key] = parseResponse.results[0]; + } + })); + continue; } - return val; - }).toList()); - loadedObjects.putIfAbsent( - key, - () => queryBuilder.query().then((ParseResponse value) => - MapEntry(key, value))); + } else { + print('recycled $key'); + //recursion + loadingNodes.add(_loadIncludes(includedObject, + oldObject: oldObject?.get(key), paths: paths[key])); + continue; + } + } else { + //All fine for this key + continue; } } - final List> responses = - await Future.wait>( - loadedObjects.values); - for (MapEntry entry in responses) { - if (entry.value.success && entry.value.results.length == 1) - object.getObjectData()[entry.key] = entry.value.results[0]; + await Future.wait(loadingNodes); + +// final List> includes = validIncludePaths; +// final List TLIncludes = includes +// .where((element) => element.length == 1) +// .map((e) => e[0]) +// .toList(); +// final Map>> loadedObjects = +// >>{}; +// for (String key in TLIncludes) { +// if (object.containsKey(key)) { +// final QueryBuilder queryBuilder = +// QueryBuilder(object.get(key)); +// queryBuilder.whereEqualTo( +// keyVarObjectId, object.get(key).objectId); +// queryBuilder.includeObject(includes +// .where((List path) => path.length > 1 && path[0] == key) +// .map((List path) { +// String val = ''; +// for (int i = 1; i < path.length; i++) { +// if (i > 1) { +// val += '.'; +// } +// val += path[i]; +// } +// return val; +// }).toList()); +// loadedObjects.putIfAbsent( +// key, +// () => queryBuilder.query().then((ParseResponse value) => +// MapEntry(key, value))); +// } +// } +// final List> responses = +// await Future.wait>( +// loadedObjects.values); +// for (MapEntry entry in responses) { +// if (entry.value.success && entry.value.results.length == 1) +// object.getObjectData()[entry.key] = entry.value.results[0]; +// } +// return object; + } + + List _toIncludeStringList(Map includes) { + final List includeList = []; + for (String key in includes.keys) { + includeList.add(key); + // ignore: avoid_as + if ((includes[key] as Map).isNotEmpty) { + includeList + .addAll(_toIncludeStringList(includes[key]).map((e) => '$key.$e')); + } } - return object; + return includeList; } - Future _objectAdded(T object, {bool loaded = true}) async { - object = await _loadIncludes(object); + Future _objectAdded(T object, + {bool loaded = true, bool fetchedIncludes = false}) async { + if (!fetchedIncludes) await _loadIncludes(object); for (int i = 0; i < _list.length; i++) { if (after(object, _list[i].object) != true) { _list.insert(i, ParseLiveListElement(object, loaded: loaded)); @@ -267,17 +366,18 @@ class ParseLiveList { Future _objectUpdated(T object) async { print(object); - object = await _loadIncludes(object); for (int i = 0; i < _list.length; i++) { if (_list[i].object.get(keyVarObjectId) == object.get(keyVarObjectId)) { + await _loadIncludes(object, oldObject: _list[i].object); if (after(_list[i].object, object) == null) { - _list[i].object = object; + _list[i].object = object?.clone(object?.toJson(full: true)); } else { _list.removeAt(i).dispose(); _eventStreamController.sink.add(ParseLiveListDeleteEvent( i, object?.clone(object?.toJson(full: true)))); - _objectAdded(object?.clone(object?.toJson(full: true))); + _objectAdded(object?.clone(object?.toJson(full: true)), + fetchedIncludes: true); } break; } From 31a498b178a8be864ec27664f12fc09d1fab65e8 Mon Sep 17 00:00:00 2001 From: maaeps Date: Sat, 4 Apr 2020 13:09:52 +0200 Subject: [PATCH 5/8] Deleted stuff --- lib/src/utils/parse_live_list.dart | 71 +----------------------------- 1 file changed, 1 insertion(+), 70 deletions(-) diff --git a/lib/src/utils/parse_live_list.dart b/lib/src/utils/parse_live_list.dart index c9c3506d0..ecdd8663f 100644 --- a/lib/src/utils/parse_live_list.dart +++ b/lib/src/utils/parse_live_list.dart @@ -86,27 +86,6 @@ class ParseLiveList { List get includes => _query.limiters['include']?.toString()?.split(',') ?? []; -// List> get validIncludePaths { -// final List> includePaths = includes -// .map>((String value) => value?.split('.')) -// .toList(); -// includePaths -// .sort((List a, List b) => a.length.compareTo(b.length)); -// return includePaths -// // ignore: unnecessary_parenthesis -// .where((List element) => (element.length == 1 || -// includePaths.any((List otherElement) { -// if (element.length - 1 == otherElement.length) { -// for (int i = 0; i < element.length - 1; i++) { -// if (element[i] != otherElement[i]) return false; -// } -// return true; -// } -// return false; -// }))) -// .toList(); -// } - Map get _includePaths { final Map includesMap = {}; @@ -238,7 +217,6 @@ class ParseLiveList { if (!includedObject.containsKey(keyVarUpdatedAt) || includedObject.objectId != object.get(key).objectId) { - print('fetch1 $key'); //fetch from web including sub objects //same as down there final QueryBuilder queryBuilder = QueryBuilder< @@ -248,7 +226,6 @@ class ParseLiveList { loadingNodes.add(queryBuilder .query() .then((ParseResponse parseResponse) { - print('fetched1 $key'); if (parseResponse.success && parseResponse.results.length == 1) { object.getObjectData()[key] = parseResponse.results[0]; @@ -256,7 +233,6 @@ class ParseLiveList { })); continue; } else { - print('recycled $key'); object.getObjectData()[key] = includedObject; //recursion loadingNodes @@ -264,7 +240,6 @@ class ParseLiveList { continue; } } else { - print('fetch2 $key'); //fetch from web including sub objects //same as up there final QueryBuilder queryBuilder = QueryBuilder< @@ -273,7 +248,6 @@ class ParseLiveList { ..includeObject(_toIncludeStringList(paths[key])); loadingNodes.add( queryBuilder.query().then((ParseResponse parseResponse) { - print('fetched2 $key'); if (parseResponse.success && parseResponse.results.length == 1) { object.getObjectData()[key] = parseResponse.results[0]; } @@ -281,7 +255,6 @@ class ParseLiveList { continue; } } else { - print('recycled $key'); //recursion loadingNodes.add(_loadIncludes(includedObject, oldObject: oldObject?.get(key), paths: paths[key])); @@ -293,46 +266,6 @@ class ParseLiveList { } } await Future.wait(loadingNodes); - -// final List> includes = validIncludePaths; -// final List TLIncludes = includes -// .where((element) => element.length == 1) -// .map((e) => e[0]) -// .toList(); -// final Map>> loadedObjects = -// >>{}; -// for (String key in TLIncludes) { -// if (object.containsKey(key)) { -// final QueryBuilder queryBuilder = -// QueryBuilder(object.get(key)); -// queryBuilder.whereEqualTo( -// keyVarObjectId, object.get(key).objectId); -// queryBuilder.includeObject(includes -// .where((List path) => path.length > 1 && path[0] == key) -// .map((List path) { -// String val = ''; -// for (int i = 1; i < path.length; i++) { -// if (i > 1) { -// val += '.'; -// } -// val += path[i]; -// } -// return val; -// }).toList()); -// loadedObjects.putIfAbsent( -// key, -// () => queryBuilder.query().then((ParseResponse value) => -// MapEntry(key, value))); -// } -// } -// final List> responses = -// await Future.wait>( -// loadedObjects.values); -// for (MapEntry entry in responses) { -// if (entry.value.success && entry.value.results.length == 1) -// object.getObjectData()[entry.key] = entry.value.results[0]; -// } -// return object; } List _toIncludeStringList(Map includes) { @@ -365,7 +298,6 @@ class ParseLiveList { } Future _objectUpdated(T object) async { - print(object); for (int i = 0; i < _list.length; i++) { if (_list[i].object.get(keyVarObjectId) == object.get(keyVarObjectId)) { @@ -487,7 +419,7 @@ class ParseLiveListElement { } abstract class ParseLiveListEvent { - ParseLiveListEvent(this._index, this._object); //, this._object); + ParseLiveListEvent(this._index, this._object); final int _index; final T _object; @@ -685,7 +617,6 @@ class _ParseLiveListElementWidgetState with SingleTickerProviderStateMixin { _ParseLiveListElementWidgetState( DataGetter loadedDataGetter, StreamGetter stream) { -// loadedData = loadedDataGetter(); _snapshot = ParseLiveListElementSnapshot(loadedData: loadedDataGetter()); if (stream != null) { _streamSubscription = stream().listen( From 0589a6b34d50ef0e5a7c57ebda027c5e5f96f01d Mon Sep 17 00:00:00 2001 From: maaeps Date: Mon, 6 Apr 2020 22:56:34 +0200 Subject: [PATCH 6/8] Adding listening on included objects to LiveList --- lib/src/utils/parse_live_list.dart | 241 ++++++++++++++++++++++++----- 1 file changed, 201 insertions(+), 40 deletions(-) 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(); From 30ab0cbfba43b447aa94c315628307e7c50c2c96 Mon Sep 17 00:00:00 2001 From: maaeps Date: Mon, 6 Apr 2020 23:16:48 +0200 Subject: [PATCH 7/8] Updated readme for ParseLiveList --- README.md | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) 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. From 8af90871011494457bbd379b10f4ebb62d490859 Mon Sep 17 00:00:00 2001 From: Maximilian Fischer <45403027+maaeps@users.noreply.github.com> Date: Mon, 6 Apr 2020 23:29:48 +0200 Subject: [PATCH 8/8] Update parse_live_list.dart --- lib/src/utils/parse_live_list.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/src/utils/parse_live_list.dart b/lib/src/utils/parse_live_list.dart index 974c84714..a5605f354 100644 --- a/lib/src/utils/parse_live_list.dart +++ b/lib/src/utils/parse_live_list.dart @@ -250,7 +250,7 @@ class ParseLiveList { if (!includedObject.containsKey(keyVarUpdatedAt) || includedObject.objectId != object.get(key).objectId) { - includedObject = object.get(key + includedObject = object.get(key); //fetch from web including sub objects //same as down there final QueryBuilder queryBuilder = QueryBuilder<