Pagination with StartAfter not working on a firestore collection group - firebase

I have the below query which keeps returning the same 5 documents on every call, instead of fetching the next group of documents as I would expect.
DocumentSnapshot<Object?>? lastFetchedDoc;
Future<void> getReviews() async {
Query<Map<String, dynamic>> query = firestore
.collectionGroup("reviews")
.where("city", whereIn: ['New York', 'Philadelphia', 'Washington'])
.orderBy('time_of_posting', descending: true) // timestamp
.limit(5);
if (lastFetchedDoc != null) {
query.startAfterDocument(lastFetchedDoc!);
}
QuerySnapshot snapshot = await query.get();
lastFetchedDoc = snapshot.docs.last;
}
Any ideas what the issue could be here.
Thanks

Calling startAfterDocument returns a new query, so you need to hold on to that return value:
if (lastFetchedDoc != null) {
query = query.startAfterDocument(lastFetchedDoc!);
}

Related

How to delete a doc from a subcollection using multiple filters in FlutterFlow?

I'm using FlutterFlow and my firestore structure is like:
users
contacts
contact_history
events
All are subcollections of "users". My rules are setup such that only users can see/edit/del their data.
I'm trying to delete a specific document from the "contact_history" subcollection, given a set of field values "eventUID" and "contactUID" but the query seems to come up empty and it never deletes the doc.
I'm writing to the console to check values and the params are being passed properly but myLen and iter stay at zero. What am I doing wrong? Do I need to reference the "users" collection somehow?
Future actDelEventContactHist(
String? myEventUID,
String? myContactUID,
) async {
int iter = 0;
int myLen = 0;
print('Begin: actDelEventContactHist -------');
print('myEventUID: $myEventUID');
print('myContactUID: $myContactUID');
WriteBatch batch = FirebaseFirestore.instance.batch();
FirebaseFirestore.instance
.collection('contact_history')
.where('contactUID', isEqualTo: myContactUID)
.where('eventUID', isEqualTo: myEventUID)
.get()
.then((querySnapshot) {
myLen = querySnapshot.docs.length;
print('myLen: $myLen');
querySnapshot.docs.forEach((document) {
iter++;
batch.delete(document.reference);
});
return batch.commit();
});
print('iter: $iter');
print('Docs deleted');
print('END: actDelEventContactHist -------');
Ok, I figured it out. I had to include the 'users' collection as it's the parent. To do so I had to import the firebase_auth package. Solution below...
import 'package:firebase_auth/firebase_auth.dart';
Future actDelEventContactHist(
String? myEventUID,
String? myContactUID,
) async {
// Add your function code here!
int iter = 0;
int myLen = 0;
print('Begin: actDelEventContactHist -------');
print('myEventUID: $myEventUID');
print('myContactUID: $myContactUID');
final myId = FirebaseAuth.instance.currentUser!.uid;
WriteBatch batch = FirebaseFirestore.instance.batch();
FirebaseFirestore.instance
.collection('users')
.doc(myId)
.collection('contact_history')
.where('contactUID', isEqualTo: myContactUID)
.where('eventUID', isEqualTo: myEventUID)
.get()
.then((querySnapshot) {
myLen = querySnapshot.docs.length;
print('myLen: $myLen');
querySnapshot.docs.forEach((document) {
iter++;
batch.delete(document.reference);
});
return batch.commit();
});
print('iter: $iter');
print('Docs deleted');
print('END: actDelEventContactHist -------');
}

Combine two streams based on field within their objects in Flutter

I have two queries for Firebase. One that returns a list of posts ("PostsModel") and one that returns a list of liked posts for a particular user ("LikedPostsModel"). I want to set an attribute called "liked" within "PostsModel" to true if it's also in the liked posts streams.
Pseudo Code:
/* Returns PostsModel, ex. [PostsModel(id: 1, liked: false), PostsModel(id: 2, liked: false), PostsModel(id: 3, liked: false), ...] */
Stream<List<PostsModel>> getListOfPosts();
/* Returns LikedPostsModel, ex. [LikedPostsModel(postsmodel_id: 2), ...] */
Stream<List<LikedPostsModel>> getListOfLikedPosts();
/* Returns combining PostsModel and LikedPostsModel, ex. [PostsModel(id: 1, liked: false), PostsModel(id: 2, liked: true), PostsModel(id: 3, liked: false), ...] */
Stream<List<LikedPostsModel>> getCombinedPosts() {
var listOfPosts = getListOfPosts();
var listOfLikedPosts = getListOfLikedPosts();
if PostsModel.id within listOfPosts is matches LikedPostsModel.postsmodel_id within listOfLikedPosts, then set PostsModel.liked = true;
return Stream<List<PostsModel>> with some fields "liked" set to true;
}
Actual Code:
My query to retrieve all posts:
Stream<List<PostsModel>> getListOfPostsStream() {
return _firestore
.collection("posts")
.snapshots()
.map((QuerySnapshot<Map<String, dynamic>> query) {
List<PostModel> postsModelList = [];
query.docs.forEach((element) {
postsModelList.add(PostsModel.fromDocumentSnapshot(element));
});
return postsModelList;
});
}
My query to retrieve liked posts:
#override
Stream<List<LikedPostsModel>> getLikedPostsByUser(User user) {
return _firestore
.collection("users")
.doc(user.id)
.collection("liked_posts")
.snapshots()
.map((QuerySnapshot<Map<String, dynamic>> query) {
List<LikedPostsModel> likedModelList = [];
query.docs.forEach((element) {
likedModelList.add(LikedPostsModel.fromDocumentSnapshot(element));
});
return likedModelList;
});
}
PostsModel
class PostsModel {
late String id;
late String bodyText;
bool liked = false;
PostsModel(
this.id,
this.bodyText,
);
...
}
LikedPostsModel
class LikedPostsModel {
late String id;
late String postsmodel_id;
bool liked;
LikedPostsModel(
this.id,
this.postsmodel_id,
this.liked
);
...
}
Any help is appreciated. Thanks!
To achieve it you should use the combineLatest2 method of the RxDart package.
I used it when developing a chat project. You know, at the conversations page of WhatsApp lists all chats and shows us the talker and the last message, message time, and seen information. My problem was my talker detail values and chat metadatas were in separate collections and I had to show the profile image and name of a talker and the last message, last message time and isSeen of the chat at the same time.
Here is an example from my chat project;
Stream<List<Chats>> getChats() {
final _userId = FirebaseAuth.instance.currentUser?.uid;
return _firestore
.collection('Chats')
.where('members', arrayContains: _userId)
.orderBy("lastMessageDate", descending: true)
.snapshots()
.map((convert) {
return convert.docs.map((f) {
String? talkerId;
var members = f.data()['members'];
if (members.length == 2) {
talkerId = members[0] != _userId ? members[0] : members[1];
}
Stream<ChatMetadata> chatStream = Stream.value(f).map<ChatMetadata>(
(document) => ChatMetadata.fromMap(
f.reference.path,
document.data(),
),
);
Stream<Talker> talkerStream = _firestore
.collection("Users")
.where('uid', isEqualTo: talkerId)
.snapshots()
.map<Talker>(
(querySnapshot) => querySnapshot.docs
.map((e) => Talker.fromMap(
e.data(),
))
.first,
);
return Rx.combineLatest2(
chatStream,
talkerStream,
(ChatMetadata chatMetadata, Talker talker) =>
Chats.combine(chatMetadata, talker),
);
});
}).switchMap((observables) {
return observables.isNotEmpty
? rx.Rx.combineLatestList(observables)
: Stream.value([]);
});
}
As you can see the getChats() method returns a Stream<List<Chats>>. The Chats model is a class that combines two other classes like ChatMetadata and Talker. Here is my Chats model;
import '/model/talker.dart';
import '/model/chat_metadata.dart';
class Chats {
String? docId;
String? talkerId;
String? talkerUsername;
String? talkerUnvan;
String? talkerProfileImage;
String? lastMessage;
DateTime? dateTime;
String? lastMessageOwner;
bool? isSeen;
List? members;
Chats({
this.docId,
this.talkerId,
this.talkerUsername,
this.talkerUnvan,
this.isSeen,
this.talkerProfileImage,
this.lastMessage,
this.dateTime,
this.lastMessageOwner,
this.members,
});
factory Chats.combine(ChatMetadata chat, Talker talker) {
return Chats(
docId: chat.docId,
talkerId: talker.id,
talkerUsername: talker.username,
talkerUnvan: talker.unvan,
isSeen: chat.isSeen,
talkerProfileImage: talker.photoUrl,
lastMessage: chat.lastMessage,
dateTime: chat.lastMessageDate,
lastMessageOwner: chat.lastMessageOwner,
members: chat.members,
);
}
}
You have to pay attention to the Rx.combineLatest2 method and switchMap method at the end of the code.
You can research all of them but in summary, the combineLatest2() merges the given Streams into a single Stream sequence by using the [combiner] function whenever any of the stream sequences emits an item. Then switchMap() converts each emitted item into a Stream using the given mapper function. Because the getChats() method has to returns the Stream<List<Chats>>.
You can store your likes in a Map rather than a List.
Stream<Map<String, bool>> getLikedPostsByUser(User user) {
return _firestore
.collection("users")
.doc(user.id)
.collection("liked_posts")
.snapshots()
.map((QuerySnapshot<Map<String, dynamic>> query) {
final likedPostsMap = HashMap<String, bool>();
query.docs.forEach((element) {
final model = LikedPostsModel.fromDocumentSnapshot(element);
likedPostsMap[model.postmodel_id] = model.liked;
});
return likedPostsMap;
});
}
Then use the resulting Map for a constant time lookup when populating your list of posts.
Stream<List<PostsModel>> getListOfPostsStream(Map<String, bool> likedPosts) {
return _firestore
.collection("posts")
.snapshots()
.map((QuerySnapshot<Map<String, dynamic>> query) {
List<PostModel> postsModelList = [];
query.docs.forEach((element) {
final model = PostsModel.fromDocumentSnapshot(element);
model.liked = likedPosts[model.id] ?? false;
postsModelList.add(model);
});
return postsModelList;
});
}
To improve performance, likes can be persisted locally so they don't have to be fetched each time your app is opened. Add a timestamp to your likes document to limit your query to changes since the last time you fetched.

Flutter Firestore Update Where

I'm trying to run a query that retrieves a single row given a where clause and updates it. I understand that Firebase doesn't support an UpdateWhere operations so I'm trying to use a Transaction instead.
I'm having difficulty making it work, maybe I'm too used to sql dbs... Here's my broken code
try {
final whereQuery = _db
.doc(userPath(user))
.collection("someInnerCollection")
.where("active", isEqualTo: true)
.limit(1);
await _db.runTransaction((transaction) async {
final entry = await transaction.get(whereQuery); // This doesn't compile as .get doesn't take in a query
await transaction.update(entry, {
"someValue": "newValue",
});
});
} catch (e) {
...
}
From the test I’ve made, I would suggest the following to achieve what you mention:
Based on the following answer:
As you can see from the API documentation, where() returns a Query object. It's not a DocumentReference.
Even if you think that a query will only return one document, you still have to write code to deal with the fact that it could return zero or more documents in a QuerySnapshot object. I suggest reviewing the documentation on queries to see examples.
After doing the query consult, you have to get the DocumentReference for that given result.
Then, you can use that reference to update the field inside a Batched writes
try {
final post = await firestore
.collection('someInnerCollection')
.where('active', isEqualTo: true)
.limit(1)
.get()
.then((QuerySnapshot snapshot) {
//Here we get the document reference and return to the post variable.
return snapshot.docs[0].reference;
});
var batch = firestore.batch();
//Updates the field value, using post as document reference
batch.update(post, { 'someValue': 'newValue' });
batch.commit();
} catch (e) {
print(e);
}
You are passing the DocumentSnapshot back in the update() operation instead of DocumentReference itself. Try refactoring the like this:
final docRefToUpdate = _db.collection("colName").doc("docId");
await _db.runTransaction((transaction) async {
final entry = await transaction.get() // <-- DocRef of document to update in get() here
await transaction.update(docRefToUpdate, {
// Pass the DocumentReference here ^^
"someValue": "newValue",
});
});
You can use a collection reference and then update single fields using .update().
final CollectionReference collectionReference = FirebaseFirestore.instance.collection('users');
await collectionReference.doc(user.uid).collection('yourNewCollection').doc('yourDocumentInsideNestedCollection').update({
'singleField': 'whatever you want,
});
Same code using "where"
collectionReference.doc(user.uid).collection('yourNewCollection').doc().where('singleField', isEqualTo: yourValue).update({
'singleField': 'whatever you want,
});

Firestore calculate values from documents

I have created a Future which prints out all of the fields from my firestore collection 'products' documents, but this collection is one of many of the same name, as seen below, and therefore I am printing out all of the values of each of the collections documents' fields. I want to add the 'productPrice' for each document that is in it's own collection 'products'
First Products Collection:
Second Products Collection:
My function is listening to all of the 'products' collections. When I try to print the productPrice field values for each of their documents, I get them all printed out, as seen below in the console. I need to add the productPrice values of 29.33 + 39.69 together to make
69.02 but I don't know how to do that without also adding the unrelated document that has the value of 149.99
final FirebaseAuth _auth = FirebaseAuth.instance;
final FirebaseFirestore _cloudStore = FirebaseFirestore.instance;
List<OrderModel> orders = [];
List<ProductModel> orderProductList = [];
Future startListening() async {
User currentUser = _auth.currentUser;
if(currentUser != null){
_cloudStore
.collection("orders")
.doc(currentUser.email)
.collection("orders")
.snapshots()
.listen((event) {
orders = event.docs.map((value){
return OrderModel(
value.data()["name"],
value.data()["date"],
value.data()["dateTime"],
value.data()["status"],
value.data()["productImage"],
);
}).toList();
for(var i = 0; i < orders.length; i++){
if(currentUser != null){
_cloudStore
.collection("orders")
.doc(currentUser.email)
.collection("orders")
.doc(orders[i].dateTime)
.collection("products")
.snapshots()
.listen((event){
orderProductList = event.docs.map((value){
return ProductModel(
value.data()['productName'],
value.data()['productQuantity'],
value.data()['productPrice'],
value.data()['productImage'],
);
}).toList();
print('separate');
event.docs.forEach((value){
var prices = value.data()['productPrice'];
print(prices);
});
notifyListeners();
});
}
}
notifyListeners();
});
} else {
return null;
}
}
My current output in the console:
Restarted application in 881ms.
Reloaded 0 of 983 libraries in 32ms.
flutter: separate
flutter: 149.99
flutter: separate
flutter: 29.33
flutter: 39.69
If I understand the question correctly, It should work if you change your code like that:
double sum = 0.0;
event.docs.forEach((value){
var prices = value.data()['productPrice'];
sum = sum + prices;
print(prices);
});
print(sum);
You can just add that after your print('separate'); statement.

Flutter Firestore query with startAfter

I am using Flutter (cloud_firestore) and trying to get data from Firestore after document with title 'xxx', but it returns 0 results.
return Firestore.instance.collection('products')
.orderBy('title')
.startAfter([{'title': 'xxx'}
]);
What am I doing wrong? How can I properly implement flutter pagination?
You should pass a value, not a map:
return Firestore.instance.collection('products')
.orderBy('title')
.startAfter(['xxx']);
The documentation on this is not particularly clear.
This is what worked for me. My orderBy is based on {FirstName, LastName & Email}. So, the startAfter should also match these field values. I saved the document in a variable and later made a list with the values of those fields and used as startAfter parameter.
static final Query queryBase = userCollectionRef
.orderBy(FieldNames.FIRST_NAME)
.orderBy(FieldNames.LAST_NAME)
.orderBy(FieldNames.EMAIL);
DocumentSnapshot _lastUser;
Future<List<DocumentSnapshot>> getAllUserDocuments({
bool next,
int limit = 10,
}) async {
List<DocumentSnapshot> _userDocSnaps = [];
try {
if (!next) _lastUser = null;
Query _query = queryBase;
if (_lastUser != null)
_query = _query.startAfter([
_lastUser.data[FieldNames.FIRST_NAME],
_lastUser.data[FieldNames.LAST_NAME],
_lastUser.data[FieldNames.EMAIL],
]);
var _userDocsSnap = await _query.limit(limit).getDocuments();
if (_userDocsSnap != null && _userDocsSnap.documents != null) {
_userDocSnaps = _userDocsSnap.documents;
if (_userDocSnaps != null && _userDocSnaps.length >= 1)
_lastUser = _userDocSnaps[_userDocSnaps.length - 1];
}
} catch (err) {
String errMessage = 'Exception in method _getAllUserDocuments';
PrintHelper.handlePrint(errMessage, err);
}
return _userDocSnaps;
}
You can pass entire doc using startAfterDocument method
_db
.collection('requirement')
.orderBy('createdAt', descending: true)
.startAfterDocument(lastVisible)

Resources