Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Firestore Paginating data + Snapshot listener

I am working with Firestore right now and have a little bit of a problem with pagination.
Basically, I have a collection (assume 10 items) where each item has some data and a timestamp.

Now, I am fetching the first 3 items like this:

Firestore.firestore()
    .collection("collectionPath")
    .order(by: "timestamp", descending: true)
    .limit(to: 3)
    .addSnapshotListener(snapshotListener())

Inside my snapshot listener, I save the last document from the snapshot, in order to use that as a starting point for my next page.

So, at some time I will request the next page of items like this:

Firestore.firestore()
    .collection("collectionPath")
    .order(by: "timestamp", descending: true)
    .start(afterDocument: lastDocument)
    .limit(to: 3)
    .addSnapshotListener(snapshotListener2()) // Note that this is a new snapshot listener, I don't know how I could reuse the first one

Now I have the items from index 0 to index 5 (in total 6) in my frontend. Neat!

If the document at index 4 now updates its timestamp to the newest timestamp of the whole collection, things start to go down.
Remember that the timestamp determines its position on account of the order clause!

What I expected to happen was, that after the changes are applied, I still show 6 items (and still ordered by their timestamps)

What happened was, that after the changes are applied, I have only 5 items remaining, since the item that got pushed out of the first snapshot is not added to the second snapshot automatically.

Am I missing something about Pagination with Firestore?

EDIT: As requested, I post some more code here:
This is my function to return a snapshot listener. Well, and the two methods I use to request the first page and then the second page I posted already above

private func snapshotListener() -> FIRQuerySnapshotBlock {
    let index = self.index
    return { querySnapshot, error in
        guard let snap = querySnapshot, error == nil else {
            log.error(error)
            return
        }

        // Save the last doc, so we can later use pagination to retrieve further chats
        if snap.count == self.limit {
            self.lastDoc = snap.documents.last
        } else {
            self.lastDoc = nil
        }

        let offset = index * self.limit

        snap.documentChanges.forEach() { diff in
            switch diff.type {
            case .added:
                log.debug("added chat at index: \(diff.newIndex), offset: \(offset)")
                self.tVHandler.dataManager.insert(item: Chat(dictionary: diff.document.data() as NSDictionary), at: IndexPath(row: Int(diff.newIndex) + offset, section: 0), in: nil)

            case .removed:
                log.debug("deleted chat at index: \(diff.oldIndex), offset: \(offset)")
                self.tVHandler.dataManager.remove(itemAt: IndexPath(row: Int(diff.oldIndex) + offset, section: 0), in: nil)

            case .modified:
                if diff.oldIndex == diff.newIndex {
                    log.debug("updated chat at index: \(diff.oldIndex), offset: \(offset)")
                    self.tVHandler.dataManager.update(item: Chat(dictionary: diff.document.data() as NSDictionary), at: IndexPath(row: Int(diff.oldIndex) + offset, section: 0), in: nil)
                } else {
                    log.debug("moved chat at index: \(diff.oldIndex), offset: \(offset) to index: \(diff.newIndex), offset: \(offset)")
                    self.tVHandler.dataManager.move(item: Chat(dictionary: diff.document.data() as NSDictionary), from: IndexPath(row: Int(diff.oldIndex) + offset, section: 0), to: IndexPath(row: Int(diff.newIndex) + offset, section: 0), in: nil)
                }
            }
        }
        self.tableView?.reloadData()
    }
}

So again, I am asking if I can have one snapshot listener that listens for changes in more than one page I requested from Firestore

like image 828
skaldesh Avatar asked Nov 08 '17 14:11

skaldesh


People also ask

What is a snapshot listener?

Listeners and Snapshots It is similar to an event handler in the sense that a code is triggered based on a certain circumstance. In our case, whenever changes in that node's data occur, the listener automatically provides the application updated data, called a snapshot.

How do I listen to changes on firestore?

You can listen to a document with the onSnapshot() method. An initial call using the callback you provide creates a document snapshot immediately with the current contents of the single document. Then, each time the contents change, another call updates the document snapshot.

What is firestore pagination?

Pagination is the process of dividing data into discrete pages. In Firestore, it is achieved by ordering a collection by a field, limiting it to a consistent page size, then offsetting the query.


2 Answers

Well, I contacted the guys over at Firebase Google Group for help, and they were able to tell me that my use case is not yet supported.
Thanks to Kato Richardson for attending to my problem!

For anyone interested in the details, see this thread

like image 57
skaldesh Avatar answered Oct 13 '22 21:10

skaldesh


I came across the same use case today and I have successfully implemented a working solution in Objective C client. Below is the algorithm if anyone wants to apply in their program and I will really appreciate if google-cloud-firestore team can put my solution on their page.

Use Case: A feature to allow paginating a long list of recent chats along with the option to attach real time listeners to update the list to have chat with most recent message on top.

Solution: This can be made possible by using pagination logic like we do for other long lists and attaching real time listener with limit set to 1:

Step 1: On page load fetch the chats using pagination query as below:

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view.
     [self fetchChats];
}

-(void)fetchChats {
    __weak typeof(self) weakSelf = self;
     FIRQuery *paginateChatsQuery = [[[self.db collectionWithPath:MAGConstCollectionNameChats]queryOrderedByField:MAGConstFieldNameTimestamp descending:YES]queryLimitedTo:MAGConstPageLimit];
    if(self.arrChats.count > 0){
        FIRDocumentSnapshot *lastChatDocument = self.arrChats.lastObject;
        paginateChatsQuery = [paginateChatsQuery queryStartingAfterDocument:lastChatDocument];
    }
    [paginateChatsQuery getDocumentsWithCompletion:^(FIRQuerySnapshot * _Nullable snapshot, NSError * _Nullable error) {
        if (snapshot == nil) {
            NSLog(@"Error fetching documents: %@", error);
            return;
        }
        ///2. Observe chat updates if not attached
        if(weakSelf.chatObserverState == ChatObserverStateNotAttached) {
            weakSelf.chatObserverState = ChatObserverStateAttaching;
            [weakSelf observeChats];
        }

        if(snapshot.documents.count < MAGConstPageLimit) {
            weakSelf.noMoreData = YES;
        }
        else {
            weakSelf.noMoreData = NO;
        }

        [weakSelf.arrChats addObjectsFromArray:snapshot.documents];
        [weakSelf.tblVuChatsList reloadData];
    }];
}

Step 2: On success callback of "fetchAlerts" method attach the observer for real time updates only once with limit set to 1.

-(void)observeChats {
    __weak typeof(self) weakSelf = self;
    self.chatsListener = [[[[self.db collectionWithPath:MAGConstCollectionNameChats]queryOrderedByField:MAGConstFieldNameTimestamp descending:YES]queryLimitedTo:1]addSnapshotListener:^(FIRQuerySnapshot * _Nullable snapshot, NSError * _Nullable error) {
        if (snapshot == nil) {
            NSLog(@"Error fetching documents: %@", error);
            return;
        }
        if(weakSelf.chatObserverState == ChatObserverStateAttaching) {
            weakSelf.chatObserverState = ChatObserverStateAttached;
        }

        for (FIRDocumentChange *diff in snapshot.documentChanges) {
            if (diff.type == FIRDocumentChangeTypeAdded) {
                ///New chat added
                NSLog(@"Added chat: %@", diff.document.data);
                FIRDocumentSnapshot *chatDoc = diff.document;
                [weakSelf handleChatUpdates:chatDoc];

            }
            else if (diff.type == FIRDocumentChangeTypeModified) {
                NSLog(@"Modified chat: %@", diff.document.data);
                FIRDocumentSnapshot *chatDoc = diff.document;
                [weakSelf handleChatUpdates:chatDoc];
            }
            else if (diff.type == FIRDocumentChangeTypeRemoved) {
                NSLog(@"Removed chat: %@", diff.document.data);
            }
        }
    }];

}

Step 3. On listener callback check for document changes and handle only FIRDocumentChangeTypeAdded and FIRDocumentChangeTypeModified events and ignore the FIRDocumentChangeTypeRemoved event. We are doing this by calling "handleChatUpdates" method for both FIRDocumentChangeTypeAdded and FIRDocumentChangeTypeModified event in which we are first trying to find the matching chat document from local list and if it exist we are removing it from the list and then we are adding the new document received from listener callback and adding it to the beginning of the list.

-(void)handleChatUpdates:(FIRDocumentSnapshot *)chatDoc {
    NSInteger chatIndex = [self getIndexOfMatchingChatDoc:chatDoc];
    if(chatIndex != NSNotFound) {
        ///Remove this object
        [self.arrChats removeObjectAtIndex:chatIndex];
    }
    ///Insert this chat object at the beginning of the array
     [self.arrChats insertObject:chatDoc atIndex:0];

    ///Refresh the tableview
    [self.tblVuChatsList reloadData];
}

-(NSInteger)getIndexOfMatchingChatDoc:(FIRDocumentSnapshot *)chatDoc {
    NSInteger chatIndex = 0;
    for (FIRDocumentSnapshot *chatDocument in self.arrChats) {
        if([chatDocument.documentID isEqualToString:chatDoc.documentID]) {
            return chatIndex;
        }
        chatIndex++;
    }
    return NSNotFound;
}

Step 4. Reload the tableview to see the changes.

like image 28
Milan Agarwal Avatar answered Oct 13 '22 20:10

Milan Agarwal