How to paginate Firestore with Android?
As it is mentioned in the official documentation, the key for solving this problem is to use the startAfter() method. So you can paginate queries by combining query cursors with the limit()
method. You'll be able to use the last document in a batch as the start of a cursor for the next batch.
To solve this pagination problem, please see my answer from this post, in which I have explained step by step, how you can load data from a Cloud Firestore database in smaller chunks and display it in a ListView
on button click.
Solution:
To get the data from your Firestore database and display it in smaller chunks in a RecyclerView
, please follow the steps below.
Let's take the above example in which I have used products. You can use products, cities or whatever you want. The principles are the same. Assuming that you want to load more products when user scrolls, I'll use RecyclerView.OnScrollListener
.
Let's define first the RecyclerView
, set the layout manager to LinearLayoutManager
and create a list. We also instantiate the adapter using the empty list and set the adapter to our RecyclerView
:
RecyclerView recyclerView = findViewById(R.id.recycler_view);
recyclerView.setLayoutManager(new LinearLayoutManager(this));
List<ProductModel> list = new ArrayList<>();
ProductAdapter productAdapter = new ProductAdapter(list);
recyclerView.setAdapter(productAdapter);
Let's assume we have a database structure that looks like this:
Firestore-root
|
--- products (collection)
|
--- productId (document)
|
--- productName: "Product Name"
And a model class that looks like this:
public class ProductModel {
private String productName;
public ProductModel() {}
public ProductModel(String productName) {this.productName = productName;}
public String getProductName() {return productName;}
}
This how the adapter class should look like:
private class ProductAdapter extends RecyclerView.Adapter<ProductViewHolder> {
private List<ProductModel> list;
ProductAdapter(List<ProductModel> list) {
this.list = list;
}
@NonNull
@Override
public ProductViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_product, parent, false);
return new ProductViewHolder(view);
}
@Override
public void onBindViewHolder(@NonNull ProductViewHolder productViewHolder, int position) {
String productName = list.get(position).getProductName();
productViewHolder.setProductName(productName);
}
@Override
public int getItemCount() {
return list.size();
}
}
The item_product
layout contains only one view, a TextView
.
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/text_view"
android:textSize="25sp"/>
And this is how the holder class should look like:
private class ProductViewHolder extends RecyclerView.ViewHolder {
private View view;
ProductViewHolder(View itemView) {
super(itemView);
view = itemView;
}
void setProductName(String productName) {
TextView textView = view.findViewById(R.id.text_view);
textView.setText(productName);
}
}
Now, let's define a limit as a global variable and set it to 15
.
private int limit = 15;
Let's define now the query using this limit:
FirebaseFirestore rootRef = FirebaseFirestore.getInstance();
CollectionReference productsRef = rootRef.collection("products");
Query query = productsRef.orderBy("productName", Query.Direction.ASCENDING).limit(limit);
Here is the code that also does the magic in your case:
query.get().addOnCompleteListener(new OnCompleteListener<QuerySnapshot>() {
@Override
public void onComplete(@NonNull Task<QuerySnapshot> task) {
if (task.isSuccessful()) {
for (DocumentSnapshot document : task.getResult()) {
ProductModel productModel = document.toObject(ProductModel.class);
list.add(productModel);
}
productAdapter.notifyDataSetChanged();
lastVisible = task.getResult().getDocuments().get(task.getResult().size() - 1);
RecyclerView.OnScrollListener onScrollListener = 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);
LinearLayoutManager linearLayoutManager = ((LinearLayoutManager) recyclerView.getLayoutManager());
int firstVisibleItemPosition = linearLayoutManager.findFirstVisibleItemPosition();
int visibleItemCount = linearLayoutManager.getChildCount();
int totalItemCount = linearLayoutManager.getItemCount();
if (isScrolling && (firstVisibleItemPosition + visibleItemCount == totalItemCount) && !isLastItemReached) {
isScrolling = false;
Query nextQuery = productsRef.orderBy("productName", Query.Direction.ASCENDING).startAfter(lastVisible).limit(limit);
nextQuery.get().addOnCompleteListener(new OnCompleteListener<QuerySnapshot>() {
@Override
public void onComplete(@NonNull Task<QuerySnapshot> t) {
if (t.isSuccessful()) {
for (DocumentSnapshot d : t.getResult()) {
ProductModel productModel = d.toObject(ProductModel.class);
list.add(productModel);
}
productAdapter.notifyDataSetChanged();
lastVisible = t.getResult().getDocuments().get(t.getResult().size() - 1);
if (t.getResult().size() < limit) {
isLastItemReached = true;
}
}
}
});
}
}
};
recyclerView.addOnScrollListener(onScrollListener);
}
}
});
In which lastVisible
is a DocumentSnapshot
object which represents the last visible item from the query. In this case, every 15'th one and it is declared as a global variable:
private DocumentSnapshot lastVisible;
And isScrolling
and isLastItemReached
are also global variables and are declared as:
private boolean isScrolling = false;
private boolean isLastItemReached = false;
If you want to get data in realtime, then instead of using a get()
call you need to use addSnapshotListener()
as explained in the official documentation regarding listening to multiple documents in a collection. More information you can find the following article:
- How to create a clean Firestore pagination with real-time updates?
FirebaseUI-Android also recently came out with a Firestore Paginator.
I have used it in my code, and it works great - just keep in mind that it operates using .get() instead of .addSnapshotListener(), so the recycler is not in realtime.
See the docs here:
https://github.com/firebase/FirebaseUI-Android/tree/master/firestore#using-the-firestorepagingadapter
You can also use FirestorePagingAdapter
provided by Firebase-UI-Firestore
You need to install this dependency
implementation 'com.firebaseui:firebase-ui-firestore:latest_version_here'
Solution
Step 1: Create a global Firestore paging adapter variable and pass the Model class and ViewHolder, and also the Model variable.
private FirestorePagingAdapter<Model, ModelViewHolder> adapter;
private Model model;
Step 2: Create a firebase query
Query query = db.collection("cities")
.orderBy("population");
Step 3: Let's build the pagedlist config. Here you will pass how much data to be queried in each page;
PagedList.Config config = new PagedList.Config.Builder()
.setEnablePlaceholders(false)
.setPrefetchDistance(10)
.setPageSize(15)
.build();
Step 4: After setting the config, let's now build the Firestore paging options where you will pass the query
and config
.
FirestorePagingOptions<Model> options = new FirestorePagingOptions.Builder<Model>()
.setLifecycleOwner(this)
.setQuery(query, config, snapshot -> {
model = snapshot.toObject(Model.class);
return model;
})
.build();
Step: 5 Now let's pass the data to the Recylerview
adapter = new FirestorePagingAdapter<Model, ModelViewHolder>(options) {
@Override
protected void onBindViewHolder(@NonNull ModelViewHolder holder, int position, @NonNull Model model) {
holder.bindTO(model);
}
@NonNull
@Override
public ModelViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.layout_model, parent, false);
return new ModelViewHolder(view);
}
@Override
protected void onError(@NonNull Exception e) {
super.onError(e);
//logic here
}
@Override
protected void onLoadingStateChanged(@NonNull LoadingState state) {
switch (state) {
case LOADING_INITIAL:
break;
case LOADING_MORE:
break;
case LOADED:
notifyDataSetChanged();
break;
case ERROR:
Toast.makeText(requireActivity(), "Error", Toast.LENGTH_SHORT).show();
//logic here
break;
case FINISHED:
//logic here
break;
}
}
};
productRecycler.setAdapter(adapter);
adapter.notifyDataSetChanged();
}
Happy Coding!