I am currently dealing with pagination with addSnapshotListener in Firebase’s firestore and it appears there’s no easy way to implement Snapshot with pagination.
Original premise:
I started the implementation with addSnapshotListener as follows:
db.collectionGroup("images")
.order(by: "createdTime")
.whereField("featured", isEqualTo: true)
.addSnapshotListener { querySnapshot, _ in
if let querySnapshot = querySnapshot {
vm.featuredImages = querySnapshot.documents.compactMap { document in
do {
let image = try document.data(as: ImageModel.self)
print("DEBUG FEATURED IMAGE: (String(describing: image))")
return image
} catch {
print("DEBUG FEATURED IMAGE ERROR (String(describing: error))")
}
return nil
}
}
}
And all goes well. The data is fetched into the ViewModel and any new changes are automatically notified via Firestore’s library and the local model gets updated.
Now add pagination:
I’ve read the official documentation as well as all the stackoverflow threads. It appears there is no easy to maintain a addSnapshotListener with a new page.
(A) One naive approach when a new page is requested would be to
- Keep track of the old listener, and then unregisters the old one
- Register a new snapshotListener now with a new page (10 -> 20
elements) - Repopulate the local model
This seems to work ok on the surface however with the one big problem is that you would be re-fetching the first 10 when you request for page 2. And the fetches become exponential as you add pages!
(B) Another solution mentioned in Firebase’s official youtube is
- Keep the old listener, but keep adding a new listener per new page
- On first fetch, it’s easy, you would just dump the new data into the old local model
- But when things update, it’s a lot of manual work. You would have to either diff the new data vs the old local model or somehow find a way to coordinate all the listeners and merging them into a new modal .
I imagine querySnapshot is the standard way of keep data in sync with apps. I imagine every app is going to need pagination. What is the correct solution?
2
Answers
A. First part is to handle new documents and display document for the first time during the pagination:
get()
query, separate from the listenerThe difficult part here is to handle the case when your app goes offline
B. Secondly, you have to handle updates on documents that are not part of the snapshot listener (all the one after the first 10).
get
query to the backend every time a new such document is scrolled to, checking if it has been upddated since the last time it was loaded. I have optimized thisget
query as follows:whenever a document is updated (including deletions) a field
time_updated
is setwhen a document is fetched my app stores locally the document’s
time_updated
aslast_pulled_updated
when a user scrolls back to a document my app will fetch it as well as the next 9 with the following query:
this costs at most 10 reads and may cost only 1 read if one or no document is returned
C. Deletions. By deletion I mean rendered innaccessible to the end user, not deleted from Firestore. So a "deleted" document could simply have a field
deleted
set to true and the listener and pagination query will both contain.whereField("deleted", isEqualTo: false)
. You can use TTL to have it really deleted later on.I always done this with solution (A) you mentioned.
This only happend when you using
.get
, But if you are attach listener to it you only get charged from those documents that haven’t fetched yet. See this question,