ZOFTINO.COM android and web dev tutorials

Firebase Pagination Using Android Paging Library

If your app has a feature which displays list of items in recycler view from firebase database and the number of items is big, implementing pagination helps in achieving quick response time and reduced network bandwidth.

In this post, you can learn how to display the data fetched from Firebase firestore in recycler view with pagination using android paging library. For details about paging library, see pagination with android paging library example.

Firebase Database Pagination Example

The example app shows ATP rankings in recycler view. The data is stored in firebase firestore database. The example app fetches the data in chunks in response to scrolling events of the list displayed in recycler view.

It loads data for 20 players initially. Subsequent pages with each page containing next 25 player’s data are loaded as user scrolls down the list.

The example uses ItemKeyedDataSource and RxPagedListBuilder. In addition to paging library, you need to add paging rxjava2 library dependency as RxPagedListBuilder is part of it.

android paging library firebase pagination example

Setup

Since example uses Firebase firestore, we need to setup firebase. For firebase setup and to know how to use Firestore database in android, see android firestore database example or other firebase tutorials for android.

After firebase project setup is complete, then add following entries in buildscript section and dependencies sub section of project’s build.gradle file.

classpath 'com.google.gms:google-services:4.1.0'
classpath 'com.google.firebase:firebase-plugins:1.1.5'

Add firebase plugin at bottom of module’s build.gradle file.

apply plugin: 'com.google.gms.google-services'

Add firebase, rxJava and paging dependencies to module’s build.gradle file.

implementation 'com.google.firebase:firebase-core:16.0.3'
implementation 'com.google.firebase:firebase-firestore:17.1.0'
implementation 'io.reactivex.rxjava2:rxjava:2.2.1'
implementation 'io.reactivex.rxjava2:rxandroid:2.1.0'
implementation 'android.arch.paging:runtime:1.0.1'
implementation 'android.arch.paging:rxjava2:1.0.1'
implementation 'android.arch.lifecycle:extensions:1.1.1'

Add internet permission to manifest file.

<uses-permission android:name="android.permission.INTERNET"/>

Data Setup

I created an activity within the same example to load ATP rankings data into firebase firestore database. You can use other options provided by firebase to load data into Firestore. Below is the activity which loads ATP rankings data into firestore database. The data file which contains fields separated by comma is saved in raw folder. The raw source is opened and processed and records are added to firestore db in the background thread.

public class LoadActivity extends AppCompatActivity {
    private static FirebaseFirestore firestoreDB;
    private static final String TAG = "LoadActivity";
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        firestoreDB = FirebaseFirestore.getInstance();
    }

    public void fillDB(View v){
        Executors.newSingleThreadScheduledExecutor().execute(new Runnable() {
            @Override
            public void run() {
                loadATPRankings();
            }
        });
    }

    public void loadATPRankings(){
        InputStream inputStream = this.getResources().openRawResource(R.raw.atp_ranks);
        Scanner sc = null;
        try {
            sc = new Scanner(inputStream);
        } catch (Exception e) {

        }
        ATPRank atpRank;

        while(sc.hasNextLine()) {
            String line = sc.nextLine();
            String atpd[] = line.split(",");

            atpRank = new ATPRank();
            atpRank.setRank(Integer.parseInt(atpd[0]));
            atpRank.setPoints(Integer.parseInt(atpd[1]));
            atpRank.setName(atpd[2]);
            addDocumentToCollection(atpRank);
        }

    }


    private void addDocumentToCollection(ATPRank ranksList ){
        try {
        firestoreDB.collection("atp_ranks").document(""+ranksList.getRank())
                .set(ranksList)
                .addOnSuccessListener(new OnSuccessListener<Void>() {
                    @Override
                    public void onSuccess(Void v) {
                        Log.d(TAG, "inserted recs to fire store db");
                    }
                })
                .addOnFailureListener(new OnFailureListener() {
                    @Override
                    public void onFailure(@NonNull Exception e) {
                        Log.d(TAG, "failed fill db");
                    }
                });
        }catch (Exception e){
            e.printStackTrace();
        }
    }
}

Firestore Repository Class

Firestore repository class fetches data from firestore using the input parameters provided by pagination data source object and passes the results back to paging component using the data source callback.

import android.arch.paging.ItemKeyedDataSource;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.util.Log;

import com.google.firebase.firestore.DocumentSnapshot;
import com.google.firebase.firestore.EventListener;
import com.google.firebase.firestore.FirebaseFirestore;
import com.google.firebase.firestore.FirebaseFirestoreException;
import com.google.firebase.firestore.QuerySnapshot;

import java.util.ArrayList;
import java.util.List;

public class FirebaseRepository {
    private FirebaseFirestore firestoreDB;
    public FirebaseRepository(){
        firestoreDB = FirebaseFirestore.getInstance();
    }
    public void getAtpRanks(final int startRank, final int size,
                            @NonNull final ItemKeyedDataSource.LoadCallback<ATPRank> callback){
        firestoreDB.collection("atp_ranks")
                .whereGreaterThan("rank", startRank)
                .limit(size).addSnapshotListener(new EventListener<QuerySnapshot>() {
            @Override
            public void onEvent(@Nullable QuerySnapshot snapshots,
                                @Nullable FirebaseFirestoreException e) {

                if (e != null) {
                    Log.w("", "exception in fetching from firestore", e);
                    return;
                }
                List<ATPRank> ranksList = new ArrayList<>();
                for(DocumentSnapshot doc : snapshots.getDocuments()){
                    ranksList.add(doc.toObject(ATPRank.class));
                }

                if(ranksList.size() == 0){
                    return;
                }
                if(callback instanceof ItemKeyedDataSource.LoadInitialCallback){
                    //initial load
                    ((ItemKeyedDataSource.LoadInitialCallback)callback)
                            .onResult(ranksList, 0, ranksList.size());
                }else{
                    //next pages load
                    callback.onResult(ranksList);
                }
            }
        });
    }
}

Paging Data Source Factory

Data source factory used by paging component to get data source object.

import android.arch.paging.DataSource;

public class ATPRanksDataSourceFactory extends DataSource.Factory<Integer, ATPRank>{
    @Override
    public DataSource<Integer, ATPRank> create() {
        return new ATPRanksDataSource();
    }
}

Data Source

Paging component uses data source to load page data. Data source calls firebase repository method to load page data by passing query parameters and callback object to it.

import android.arch.paging.ItemKeyedDataSource;
import android.support.annotation.NonNull;

public class ATPRanksDataSource extends ItemKeyedDataSource<Integer, ATPRank> {

    private FirebaseRepository firebaseRepository;

    public ATPRanksDataSource(){
        firebaseRepository = new FirebaseRepository();
    }

    @Override
    public void loadInitial(@NonNull LoadInitialParams<Integer> params,
                            @NonNull LoadInitialCallback<ATPRank> callback) {

        firebaseRepository.getAtpRanks(0, params.requestedLoadSize, callback);
    }

    @Override
    public void loadAfter(@NonNull LoadParams<Integer> params,
                          @NonNull LoadCallback<ATPRank> callback) {
        firebaseRepository.getAtpRanks(params.key, params.requestedLoadSize, callback);
    }

    @Override
    public void loadBefore(@NonNull LoadParams<Integer> params,
                           @NonNull LoadCallback<ATPRank> callback) {

    }

    @NonNull
    @Override
    public Integer getKey(@NonNull ATPRank atpRank) {
        return atpRank.getRank();
    }
}

ViewModel

View model object contains a method which creates PagedList observable object using RxPagedListBuilder and returns it.

import android.arch.lifecycle.ViewModel;
import android.arch.paging.PagedList;
import android.arch.paging.RxPagedListBuilder;

import io.reactivex.Observable;

public class ATPViewModel extends ViewModel {

    private ATPRanksDataSourceFactory dataSourceFactory;
    private PagedList.Config config;
    public ATPViewModel() {
        dataSourceFactory = new ATPRanksDataSourceFactory();

        config = (new PagedList.Config.Builder()).setEnablePlaceholders(false)
                .setInitialLoadSizeHint(20)
                .setPageSize(25).build();
    }

    public Observable<PagedList> getPagedListObservable(){
        return new RxPagedListBuilder(dataSourceFactory, config).buildObservable();
    }
}

RecyclerView Adapter

import android.arch.paging.PagedListAdapter;
import android.support.annotation.NonNull;
import android.support.v7.util.DiffUtil;
import android.support.v7.widget.RecyclerView;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import zoftino.com.firestore.R;


public class ATPRanksRVAdapter extends PagedListAdapter<ATPRank,
                                ATPRanksRVAdapter.ATPRankViewHolder> {

    public ATPRanksRVAdapter() {
        super(DIFF_CALLBACK);
    }

    public static DiffUtil.ItemCallback<ATPRank> DIFF_CALLBACK =
            new DiffUtil.ItemCallback<ATPRank>() {
                @Override
                public boolean areItemsTheSame(@NonNull ATPRank rank,
                                               @NonNull ATPRank rankTwo) {
                    return rank.getRank() == rankTwo.getRank();
                }

                @Override
                public boolean areContentsTheSame(@NonNull ATPRank rank,
                                                  @NonNull ATPRank rankTwo) {
                    return rank.equals(rankTwo);
                }
            };

    @Override
    public ATPRankViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        LayoutInflater li = LayoutInflater.from(parent.getContext());
        View view = li.inflate(R.layout.rank_item, parent, false);
        return new ATPRankViewHolder(view);
    }

    @Override
    public void onBindViewHolder(ATPRankViewHolder holder, int position) {
        ATPRank atpRank = getItem(position);
        if(atpRank != null) {
            holder.bindTO(atpRank);
        }
    }

    public class ATPRankViewHolder extends RecyclerView.ViewHolder {
        private TextView rank;
        private TextView playerName;
        private TextView points;

        public ATPRankViewHolder(View view) {
            super(view);
            rank = view.findViewById(R.id.rank_tv);
            playerName = view.findViewById(R.id.player_tv);
            points = view.findViewById(R.id.points_tv);
        }

        public void bindTO(ATPRank atpRank){
            rank.setText(""+atpRank.getRank());
            playerName.setText(atpRank.getName());
            points.setText(""+atpRank.getPoints());
        }
    }
}

Data Model Object

public class ATPRank {
    private int rank;
    private int points;
    private String name;

    public int getRank() {
        return rank;
    }

    public void setRank(int rank) {
        this.rank = rank;
    }

    public int getPoints() {
        return points;
    }

    public void setPoints(int points) {
        this.points = points;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}

Activity

import android.arch.lifecycle.ViewModelProviders;
import android.arch.paging.PagedList;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.DividerItemDecoration;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;

import io.reactivex.Observer;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.disposables.Disposable;
import io.reactivex.schedulers.Schedulers;
import zoftino.com.firestore.atp.ATPRanksRVAdapter;
import zoftino.com.firestore.atp.ATPViewModel;

public class ATPRanksActivity extends AppCompatActivity {

    private static final String TAG = "ATPRanksActivity";
    private ATPRanksRVAdapter adapter;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.atp_activity);

        ATPViewModel viewModel = ViewModelProviders.of(this).get(ATPViewModel.class);

        RecyclerView recyclerView = findViewById(R.id.atp_ranks_rv);
        adapter = new ATPRanksRVAdapter();
        recyclerView.setAdapter(adapter);

        viewModel.getPagedListFlowable().subscribeOn(Schedulers.io())
                .observeOn(AndroidSchedulers.mainThread())
                .subscribe(observer);
                .subscribe(pagedList -> adapter.submitList(pagedList));



        recyclerView.setLayoutManager(new LinearLayoutManager(this));

        DividerItemDecoration dividerItemDecoration =
                new DividerItemDecoration(recyclerView.getContext(),
                        LinearLayoutManager.VERTICAL);
        recyclerView.addItemDecoration(dividerItemDecoration);
    }
}

Item Layout

<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_marginLeft="8dp">

    <TextView
        android:id="@+id/rank_tv"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginBottom="8dp"
        android:layout_marginTop="8dp"
        android:text="rank"
        android:textAppearance="@style/TextAppearance.AppCompat.Large"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toLeftOf="@+id/player_tv"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintVertical_bias="0.494" />

    <TextView
        android:id="@+id/player_tv"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginBottom="8dp"
        android:layout_marginLeft="16dp"
        android:layout_marginTop="8dp"
        android:text="rank"
        android:textColor="@color/colorAccent"
        android:textAppearance="@style/TextAppearance.AppCompat.Large"
        app:layout_constraintLeft_toRightOf="@+id/rank_tv"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent"/>

    <TextView
        android:id="@+id/points_l"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginBottom="8dp"
        android:layout_marginLeft="32dp"
        android:layout_marginTop="16dp"
        android:text="Points:"
        android:textAppearance="@style/TextAppearance.AppCompat.Large"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toLeftOf="@+id/player_tv"
        app:layout_constraintTop_toBottomOf="@+id/player_tv" />

    <TextView
        android:id="@+id/points_tv"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginBottom="8dp"
        android:layout_marginLeft="4dp"
        android:layout_marginTop="16dp"
        android:text="rank"
        android:textAppearance="@style/TextAppearance.AppCompat.Medium"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toRightOf="@+id/points_l"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/player_tv" />
</android.support.constraint.ConstraintLayout>

Activity Layout

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context="zoftino.com.firestore.ATPRanksActivity">
    <android.support.v7.widget.RecyclerView
        android:id="@+id/atp_ranks_rv"
        android:layout_width="match_parent"
        android:layout_height="match_parent">
    </android.support.v7.widget.RecyclerView>
</LinearLayout>