2

I have a question about how to properly paginate queries with Firestore.

By putting the next query into the OnSuccessListener of the previous query, like in the example on the Firestore page, wouldn't it inevitably always trigger a chain reaction that loads all pages at once? Isn't that something we want to avoid with pagination?

// Construct query for first 25 cities, ordered by population
Query first = db.collection("cities")
        .orderBy("population")
        .limit(25);

first.get()
    .addOnSuccessListener(new OnSuccessListener<QuerySnapshot>() {
        @Override
        public void onSuccess(QuerySnapshot documentSnapshots) {
            // ...

            // Get the last visible document
            DocumentSnapshot lastVisible = documentSnapshots.getDocuments()
                    .get(documentSnapshots.size() -1);

            // Construct a new query starting at this document,
            // get the next 25 cities.
            Query next = db.collection("cities")
                    .orderBy("population")
                    .startAfter(lastVisible)
                    .limit(25);

            // Use the query for pagination
            // ...
        }
    });

Source: https://firebase.google.com/docs/firestore/query-data/query-cursors

This is my approach. I know in a real app I should use a RecyclerView, but I just want to test it on a TextView. I want to load 3 documents with every button click. Does it make sense to store the lastResult as a member and then check if it is not null, to see if it is the first query?

public void loadMore(View v) {
    Query query;
    if (lastResult == null) {
        query = notebookRef.orderBy("priority")
                .orderBy("title")
                .limit(3);
    } else {
        query = notebookRef.orderBy("priority")
                .orderBy("title")
                .startAfter(lastResult)
                .limit(3);
    }
    query.get().addOnSuccessListener(new OnSuccessListener<QuerySnapshot>() {
        @Override
        public void onSuccess(QuerySnapshot queryDocumentSnapshots) {
            String data = "";

            for (QueryDocumentSnapshot documentSnapshot : queryDocumentSnapshots) {
                Note note = documentSnapshot.toObject(Note.class);
                note.setDocumentId(documentSnapshot.getId());

                String documentId = note.getDocumentId();
                String title = note.getTitle();
                String description = note.getDescription();
                int priority = note.getPriority();

                data += "ID: " + documentId
                        + "\nTitle: " + title + "\nDescription: " + description
                        + "\nPriority: " + priority + "\n\n";
            }

            if (queryDocumentSnapshots.size() > 0) {
                data += "_____________\n\n";
                textViewData.append(data);


                lastResult = queryDocumentSnapshots.getDocuments()
                        .get(queryDocumentSnapshots.size() - 1);
            }
        }
    });
}
Florian Walther
  • 6,237
  • 5
  • 46
  • 104

2 Answers2

4

You have to use ScrollListener in you recyclerview/listview. on start you are fetching 25 data limit, once user scroll to end of the page again you have to make new firestore call with limit(whatever you are keeping). but at this time you have to keep use startAt() in your query. input to startAt() will be your last key from the first fetched data.Its just basic overview. You can refer this link for query.

You can create pagination in recyclerview/listview with firestore as follow:

Basically Follow these steps:

1) On opening Activity/Fragment your first query will fetch 25 data limit

Query first = db.collection("cities")
        .orderBy("population")
        .limit(25);

first.get().addOnSuccessListener(new OnSuccessListener<QuerySnapshot>() {
        @Override
        public void onSuccess(QuerySnapshot documentSnapshots) {
            // add data to recyclerView/listview

            // Get the last visible document
            DocumentSnapshot lastVisible = documentSnapshots.getDocuments()
                    .get(documentSnapshots.size() -1);
        }
    });

2) Now overwrite onScrollListener of adapter

boolean isEndChildResults = false;
    mRecyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
                @Override
                public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
                    super.onScrollStateChanged(recyclerView, newState);
                    if (newState == AbsListView.OnScrollListener.SCROLL_STATE_TOUCH_SCROLL) {
                        isScrolling = true;
                    }
                }
   @Override
        public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
            super.onScrolled(recyclerView, dx, dy);
            currentVisibleItem = linearLayoutManager.getChildCount();
            totalItem = linearLayoutManager.getItemCount();
            scrolledItem = linearLayoutManager.findFirstVisibleItemPosition();
            if (isScrolling && (currentVisibleItem + scrolledItem == totalItem) && !isEndChildResults && documentSnapshot != null) {
                isScrolling = false;
                mProgressBarScroll.setVisibility(View.VISIBLE);

                FirebaseFirestore firebaseFirestore = FirebaseFirestore.getInstance();

                Query query = firebaseFirestore.collection(...).document(...).limit(25).orderBy(...).startAt(lastVisible);
                query.get().addOnCompleteListener(new OnCompleteListener<QuerySnapshot>() {
                    @Override
                    public void onComplete(@NonNull Task<QuerySnapshot> task) {

                        if (task.isSuccessful()) {
                          // add data to recyclerView/listview
                          lastVisible = documentSnapshots.getDocuments().get(documentSnapshots.size() -1);


                     if (task.getResult().size() < postPerPageLimit) {
                       // if your result size is less than your query size which means all the result has been displayed and there is no any other data to display 
                                    isEndChildResults = true;
                                }
                            }

                        }
                    }
                });

          if(isEndChildResults){
         // show snackbar/toast
            }
         }

*lastVisible documentSnapshot will change on each scroll and it will fetch data from lastVisible snapshot

Rohit Maurya
  • 730
  • 1
  • 9
  • 22
  • But the example shown above retrieves the next query in the OnSuccessListener of the first query, so it's not available outside of this anonymous class. – Florian Walther May 16 '18 at 11:34
  • @FlorianWalther i have updated my answer, let me know if you are still having any doubt. – Rohit Maurya May 16 '18 at 12:59
  • Thank you, but then lastVisible has to be a member variable, right? Because right now it is declared in the scope of the first `OnSuccessListener` – Florian Walther May 16 '18 at 16:04
  • @FlorianWalther Yes, it has to be member variable. – Rohit Maurya May 16 '18 at 17:51
  • @RohitMaurya Maybe you can also take a look at [this](https://stackoverflow.com/questions/50592325/is-there-a-way-to-paginate-queries-by-combining-query-cursors-using-firestorerec). Thanks! – Joan P. Jun 01 '18 at 11:29
  • @RohitMaurya How is `isEndChildResults` declared? How to get this variable? Thanks! – Joan P. Jun 07 '18 at 10:21
  • @IoanaP. i am using this boolean `isEndChildResults` to show snackbar in end of layout if all result being displayed and there is no next data is remain to fetch, and i have updated my answer also, have a look. – Rohit Maurya Jun 07 '18 at 11:02
  • @RohitMaurya This is what I'm interested in, to know when there is no next data is remain to fetch. In your code, you are only using `!isEndChildResults`. Is this a result of a method? – Joan P. Jun 07 '18 at 11:08
1

Constructing a query does not yet read data from that query.

So this code merely creates a query:

Query next = db.collection("cities")
        .orderBy("population")
        .startAfter(lastVisible)
        .limit(25);

It does not read any data from the database. This means it also doesn't fire any onSuccess method yet.

If you immediately next.get().addOnSuccessListener(... you'd indeed create a loop that loads all page.

Frank van Puffelen
  • 565,676
  • 79
  • 828
  • 807
  • But the query variable is declared inside the anonymous inner class, so it's not accessible from the outside right? So how would I go about using it later? – Florian Walther May 16 '18 at 14:21
  • It's just an example of how to construct such a query. – Frank van Puffelen May 16 '18 at 15:33
  • So in a real example we would store the next query in a member variable? – Florian Walther May 16 '18 at 15:38
  • That one way. Or just keep the `lastVisible` in a member and construct the query when needed. – Frank van Puffelen May 16 '18 at 15:47
  • Yes, I meant lastVisible, sorry. This variable will be null if there was no query yet, so if I want the same button to do the first loading as well as all subsequent ones, I would make a null check and create 2 different queries depending on the result? One Query with startAfter and one without. – Florian Walther May 16 '18 at 15:54
  • I wish there was some proper examples for this. Because the one given doesn't really make any sense. – Florian Walther May 16 '18 at 17:03
  • In my eyes the example in the docs is one of the clearer examples I've seen of how to build a next-page query in Firestore. It's definitely easier to read than most of the samples I've given in answers here. Can you explain what part has you stumped? – Frank van Puffelen May 16 '18 at 17:37
  • But it creates the next query directly within the OnSuccessListener of the first query, we wouldn't do this in a real app. I would love to see a real example but I find zero in Google. – Florian Walther May 16 '18 at 17:50
  • The sample is pretty close to what I'd do in a real app (although I typically work hard to avoid pagination with Firebase/Firestore altogether). If you're having a hard time translating it to your app, I recommend posting what you've tried and where you got stuck. – Frank van Puffelen May 16 '18 at 17:58
  • I posted my approach. It's not a real app, I am just testing on a TextView. I don't know if it is the right way I did it. The if check and the 2 different queries seems awkward. – Florian Walther May 16 '18 at 18:10
  • Can you maybe take a quick look at my approach – Florian Walther May 17 '18 at 06:48
  • Looks fine to me. – Frank van Puffelen May 17 '18 at 13:11
  • Thank you, appreciate it – Florian Walther May 17 '18 at 13:29