How to create a clean Firestore pagination with real-time updates?

Alex Mamo
Alex Mamo
Nov 2 · 5 min read

Why to use pagination?
Because we cannot load all the data from a local database or a backend server at once, we need to get it in smaller chunks.

Why with real-time updates?
Because there are moments when we need to get data as it becomes available. A simple example would be an app that displays ongoing matches. So we’ll use the real-time feature to know the score everytime it is changed.

In this article, I’ll explain the entire pagination algorithm. We’ll create a very simple application with a single activity where we’ll display in a RecyclerView some products that exist in a Firestore collection. This is how the database structure looks like:

We’ll create an upgraded version of my answer from stackoverflow.com but this time with the real-time feature and using MVVM architecture pattern. Let’s get started.

Since the activity is designed only for the views, we are not going to add much functionality in it. We’ll only init the RecyclerView:

productsRecyclerView = findViewById(R.id.products_recycler_view);

And the Adapter which is set using an empty list:

productsAdapter = new ProductsAdapter(productList);
productsRecyclerView.setAdapter(productsAdapter);

I’m not gonna add the content for the adapter class because it’s a very simple class that extends RecyclerView.Adapter. Beside that, at the end of the article you’ll find a link that points to the repo.

Now, in order to get the products and populate the RecyclerView, we have to create a custom LiveData class. This is very important because is the place where all the magic happens. To have real-time updates, we need to use an EventListenerer.We not gonna implement this interface in our activity but in the ProductListLiveData class. Please also note that the creation of a new instance of this LiveData is not handled in the activity but in a Repository class. You’ll see that later in this article how is done.

Because our ProductListLiveData class extends LiveData<Operation> and implements EventListener<QuerySnapshot>, there are three methods that must be implemented onActive(), onInactive() and onEvent():

As you can see, the constructor of the class has three arguments, a Firestore Query and two Callbacks, one of them helps us know which is the last visible product in the list and the second one helps us know when we reached the last product in the list.

The first method onActive() gets called by the LiveData internal infrastructure, when the number of observers becomes 1, meaning that there is an active observer attached to this LiveData. On the other hand, onInactive() is called when the number of observers gets back to 0, meaning that no one is interested in this data anymore. So in the first method we attach the listener while in the second we remove it according to the life-cycler of the activity.

What we are trying to achieve when implementing the EventListenererinterface is that we want to make this class be it’s own listener. I think that you already noticed that the type of our LiveData is Operation. This is a simple Java class that contains two fields:

Now in the onEvent() method, we get the QuerySnapshot object. To know which document is changed, we iterate using getDocumentChanges() method to get a DocumentChange. According to its type, we create then an Operation object that can be passed directly to the setValue() method. This method triggers all the observers that are interested in this data. If there are more obervers, all of them will be invoked.

Who’s creating a new instance of ProductListLiveData?
If you see above, we need in the constructor a Firestore Query object which is basically apart of the Firestore implementation process. As you know, we cannot put this directly into an activity as it will be considered poor architecture. This is because the activity is a View and the View knows about the data source (Firestore) and this is bad. We will not do that! So whoever is responsible for creating a new instance for this LiveData, needs to know about Firestore and for sure it cannot be the activity. So we definetely we need another level of protection.

To always keep the results that we get from the database we need to go back to the ViewModel, which is another Android Architecture Component. Now all we have to do is to subclass ViewModel and create a method that returns a the ProductListLiveData that we earlier talked about.

As you can see, we are also creating a repository which is basically an abstraction around the database layer. So we create the ProductListRepository interface to abstract the actual details about our Firestore database. So in this case, we’ll get the LiveData directly from the repository and return it. Now our FirestoreProductListRepository needs an implementation:

First we implement the getProductListLiveData() method from the repository interface and the methods from both callbacks. We can only return a new LiveData object if we didn’t reach the last product in the list. According to the page that is displayed, we can use the simple Query or the one that contains a startAfter(lastVisibleProduct) call. So everything about Firestore is abstracted in this repository.

Till now, we have 4 Android Architecture Component working together. The LiveData helps us propagate the changes of documents to the UI. The ViewModel helps us retain the LiveData between configuration changes. The Repository that knows how to deal with the data source and the Firestore database which is basically hosting our products.

Now we need a ViewModel, so we need to go back to the View. The ViewModel is created using the new ViewModelProvider class:

productListViewModel = new ViewModelProvider(this)
.get(ProductListViewModel.class);

Since we have the ViewModel object, now we can simply call getProductListLiveData() method. I have added everything in a single method:

Once we have the LiveData object we can set an observer on it. Inside the observer we get the Operation objects. Acording to the type of the operation (added, modified or deleted), we can call one of the there methods. The first method adds a product to the list, the second replaces a product in the list and the third removes a product from the list. The above method is called twice, once from onCreate() method when the app starts and once when we scroll and we reach the LIMIT, which in our case is set to 15. To know when we reach the LIMIT, we need to use a scroll listener:

Inside onScrolled() method we call the method again but only when the boolean isScrolling is true along with the following statement:

firstVisibleProductPosition + visibleProductCount == 
totalProductCount

This is called a clean architecture, where the repository knows nothing about the views and the views know nothing about their data source.

How about the errors?
To keep this article in reasonable size, I didn’t add the error handling but you should definetely add it in your projects. Since our listener will return the data or an Exception, we can create a new Java class (OperationOrException) containing two fields, one of type Operation and one of type FirebaseFirestoreException. In this case, you won’t use a LiveData<Operation> anymore, we’ll use a LiveData<DataOrException>. So remeber that when using Firestore, we have either an error or an success, not both, not either.

This is a demo video on how this app looks like. See, there is no “blink” when we change a price in the Firebase console.

You can find the full source code here.

#BetterTogether

Firebase Tips & Tricks

A collection of articles, recipes and tips & tricks on how to develop with Firebase, by a group of Firebase developers active on Stack Overflow

Alex Mamo

Written by

Alex Mamo

Firebase Developer. Speaker and Writer. Android Enthusiast. Java/Android Trainer.

Firebase Tips & Tricks

A collection of articles, recipes and tips & tricks on how to develop with Firebase, by a group of Firebase developers active on Stack Overflow

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade