Flutter Firestore pagination in abstract service class

570

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.

Share:
570
Théo Champion
Author by

Théo Champion

Updated on December 30, 2022

Comments

  • Théo Champion
    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 using startAfterDocument(). This means, as my database services are stateless, I would need to save this DocumentSnapshot 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
    Théo Champion almost 3 years
    I'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