Flutter Firestore pagination in abstract service class
I stumbled upon the exact same issue, even though I was using Bloc instead of Riverpod. I wrote a whole article on that, in order to support also live updates to the list and allowing infinite scrolling: ARTICLE ON MEDIUM
My approach was to order the query by name and id (for example), and using startAfter
instead of startAfterDocument
.
For example:
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:infite_firestore_list/domain/list_item_entity.dart';
import 'package:infite_firestore_list/domain/item_repository.dart';
class FirebaseItemRepository implements ItemRepository {
final _itemsCollection = FirebaseFirestore.instance.collection('items');
@override
Future<Stream<List<ListItem>>> getItems({
String startAfterName = '',
String startAfterId = '',
int paginationSize = 10,
}) async {
return _itemsCollection
.orderBy("name")
.orderBy(FieldPath.documentId)
.startAfter([startAfterName, startAfterId])
.limit(paginationSize)
.snapshots()
.map((querySnapshot) => querySnapshot.docs.map((doc) {
return ListItemDataModel.fromFirestoreDocument(doc).toDomain();
}).toList());
}
}
in this way in your logic you only have to use id and name or whatever fields you wish to use, for example a date.
If you use a combination of multiple orderBy
, the first time you run the query, Firebase may ask you to build the index with a link that will appear in the logs.
The drawback of this approach is that it only works if you are sure that the fields you are using in the orderBy
are uniques. In fact, if for example you sort by date, if two fields have the same date and you use startAfter
that date (first item), you may skip the second item with the same date...
In my example, the startAfterId doesn't seem useful, but in the usecase I had, it solved some edgecases I stumbled upon.
Alternative
An alternative I thought but that I personally didn't like (hence I did not mention it in my article) could be to store an array of the snapshots of the last documents of each page in the repository itself.
Than use the id from the logic domain to request a new page and make the correspondance id <--> snapshot
in the repository itself.
This approach could be interesting if you are expecting a finite amount of pages and hence a controlled array in your repository singleton, otherwise it smell memory leaking and that's why I personally do not like this approach to stay as general as possible.
Théo Champion
Updated on December 30, 2022Comments
-
Théo Champion over 1 year
I'm implementing pagination for my Flutter app with Firestore and I am running into a design issue.
I'm using services class to abstract database operation from the business logic of my app through data model class like so:
UI <- business logic (riverpod) <- data model class <- stateless firestore service
This works great as it follows the separation of concerns principles.
However, in the Firestore library, the only way to implement pagination is to save the last
DocumentSnapshot
to reference it in the next query usingstartAfterDocument()
. This means, as my database services are stateless, I would need to save thisDocumentSnapshot
in my business logic code, who should in principle be completely abstracted from Firestore.My first instinct would be to reconstruct a
DocumentSnapshot
from my data model class inside the service and use that for the pagination, but I would not be able to reconstruct it completely so I wonder if that would be enough.Has anyone run into this issue? How did you solve it?
Cheers!
-
Théo Champion almost 3 yearsI've explored these two options already. The first one is what I settled on so far, the only downside is that it requires every "timestamp indexed" collections to also have and index on the ID field in firestore