ZOFTINO.COM android and web dev tutorials

Pagination in Android Using Paging Library

Like large or unlimited number of data items is loaded and displayed in pages in web applications, chunks of data can be loaded and displayed in recycler view using paging library in android.

Pagination reduces the network bandwidth and consumes fewer resources because chunks of data is loaded and displayed instead of entire set of data as user scrolls down the list.

In this post, all details with examples about paging library such as how it works, components involved in pagination, data flow, and implementing pagination with local data base as source of data using Room are covered.

Table of Contents

Setup

To include paging library in your project, you need to add below entry to build.gradle file.

def paging_version = "1.0.0"
implementation "android.arch.paging:runtime:$paging_version"

Since RecyclerView, Room, ViewModle and LiveData are used in the examples, we need to add following entries as well.

    implementation 'com.android.support:recyclerview-v7:28.0.0-rc01'

    def lifecycle_version = "1.1.1"
    implementation "android.arch.lifecycle:extensions:$lifecycle_version"

    def room_version = "1.1.1"
    implementation "android.arch.persistence.room:runtime:$room_version"
    annotationProcessor "android.arch.persistence.room:compiler:$room_version"

How It Works

A collection which acts as mediator between UI and Database uses data source to asynchronously fetch page data and populates itself with the loaded data. The collection is passed to UI components thru listeners for displaying it in UI.

The collection or list is provided with configuration which contains initial load size and page size. The list manages page parameters and passes them to data source so that it can use it to load data according to configuration.

In response to UI events, UI component sends signal to the collection for next page. The collection fetches data for next page and observer or listener passes the loaded data to UI component for displaying.

Paging Library Components

The collection mentioned above is PagedList class in paging library and it is created using LivePagedListBuilder. LivePagedListBuilder is supplied with data source factory and page list configuration objects.

PagedList object is passed to recycler view adapter which can handle PagedList object. PagedList object is passed to recycler view by creating LiveData of PagedList. The observer of LiveData passes the PagedList object to the recycler view adapter which takes care of displaying data in RecyclerView, sending next page signal to PagedList and listening to PagedList for data updates.

The important behavior of PagedList is that it can’t reload records if data is updated in the database. To show updates of already loaded data, new PagedList object has to be created.

DataSource.Factory which is passed to LivePagedListBuilder is a factory for data source. You need to create DataSource.Factory and override create() method which returns DataSource object.

DataSource is used for loading data by PagedList. You can create DataSource by extending one of the three data source classes such as PageKeyedDataSource, ItemKeyedDataSource, or PositionalDataSource. You need to implement loadInitial and loadAfter methods. Using parameters in loadInitial and loadAfter methods, you can prepare query and load data.

PageKeyedDataSource and ItemKeyedDataSource can be used to load data based on key and size. They differ the way last loaded key is captured and passed as parameter to next call.

PositionalDataSource interacts with source of data which can provide data at any position and loads required number of items.

PagedListAdapter is a recycler view adapter for displaying data from PagedList. Initial data is passed to PagedListAdapter by calling submitList on the adapter and passing the list.

PagedListAdapter calls loadAround on PagedList object to make PagedList load data for next pages as user scrolls the items in recycler view. PagedListAdapter listens for data changes and displays the changes in recycler view.

Paging Library Example

Now let’s see how to implement pagination in android using the paging library. For this, I’ll take coupons application which displays list of coupons in recycler view. It loads 20 coupons initially and as user scroll down the recycler view, it will fetch 25 coupons each time.

This example fills up local data base when it is created using Room. You can create back ground process to fill it from remote source, see inserting remote data into local db example.

The example uses PageKeyedDataSource and uses record id as page key. You can use ItemKeyedDataSource as well.

android paging library example

DataSource Factory

You need to extend DataSource Factory class provided by paging library and implement create() method which returns DataSource.

import android.arch.paging.DataSource;
import android.content.Context;

/**
 * data source factory passed to PageList which calls create method to get
 * data source object
 */
public class CouponsDataSourceFactory extends DataSource.Factory<Integer, Coupon>  {
    private Context ctx;
    private CouponsDataSource couponsDataSource;
    
    public CouponsDataSourceFactory(Context ctx){
            this.ctx = ctx;
    }
    @Override
    public DataSource<Integer, Coupon> create() {
        if(couponsDataSource == null){
            couponsDataSource = new CouponsDataSource(ctx);
        }
        return couponsDataSource;
    }
}

DataSource

DataSource returns data to PageList. For our example, data source is created by extending PageKeyedDataSource class as we are going to use record id as key. You can use ItemKeyedDataSource as well. We need to override loadInitial() and loadAfter() methods of PageKeyedDataSource. In the data source, we use Room DAO to load data from the local database. In both the callack methods loadInitial() and loadAfter(), you can obtain the key from load parameters passed to them and pass the key to DAO to load data corresponding to the page being requested.

Our example initially loads records from 0 to 20 and then for next pages, it loads 25 records starting from the last loaded data.

import android.arch.paging.PageKeyedDataSource;
import android.content.Context;
import android.support.annotation.NonNull;

import java.util.List;

//data source for PagedList, it is used for loading data for each page
public class CouponsDataSource extends PageKeyedDataSource<Integer, Coupon> {
    private CouponLocalDAO couponDAO;

    public CouponsDataSource(Context ctx){
        couponDAO = LocalRepository.getCouponDB(ctx).couponDAO();
    }
    //is called too load initial data
    @Override
    public void loadInitial(@NonNull LoadInitialParams<Integer> params,
                            @NonNull LoadInitialCallback<Integer, Coupon> callback) {

        List<Coupon> cpns = couponDAO.getCouponsBySize(0, params.requestedLoadSize);

        //this is required to handle first request after db is created or app is installed
        int noOfTryies = 0;
        while(cpns.size() == 0){
            cpns = couponDAO.getCouponsBySize(0, params.requestedLoadSize);
            noOfTryies++;
            if(noOfTryies == 6){
                break;
            }
            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {  }
        }

        callback.onResult(cpns,null,
                cpns.size()+1);

    }

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

    }

    //is called to load pages of data using key passed in params
    @Override
    public void loadAfter(@NonNull LoadParams<Integer> params,
                          @NonNull LoadCallback<Integer, Coupon> callback) {
        List<Coupon> cpns = couponDAO.getCouponsBySize( params.key, params.requestedLoadSize);
        int nextKey = params.key+cpns.size();
        callback.onResult(cpns, nextKey);
    }
}

ViewModel

It contains view model factory which is need so that context can be passed to ViewModel as context is needed for initializing Room database.

In ViewModel constructor, data source factory defined above is instantiated and page list config object is created. Factory and config objects are passed to LivePagedListBuilder to build LiveData of PagedList object.

mport android.app.Application;
import android.arch.lifecycle.LiveData;
import android.arch.lifecycle.ViewModel;
import android.arch.lifecycle.ViewModelProvider;
import android.arch.paging.LivePagedListBuilder;
import android.arch.paging.PagedList;

public class CouponViewModel extends ViewModel {
    //PagedList controls data loading using data source
    public LiveData<PagedList<Coupon>> couponList;

    public CouponViewModel(Application application){

        //instantiate CouponsDataSourceFactory
        CouponsDataSourceFactory factory = new CouponsDataSourceFactory(application);

        //create PagedList Config
        PagedList.Config config = (new PagedList.Config.Builder()).setEnablePlaceholders(true)
                                .setInitialLoadSizeHint(20)
                                .setPageSize(25).build();

        //create LiveData object using LivePagedListBuilder which takes
        //data source factory and page config as params
        couponList = new LivePagedListBuilder<>(factory, config).build();
    }

    //factory for creating view model,
    // required because we need to pass Application to view model object
    public static class CouponViewModelFactory extends ViewModelProvider.NewInstanceFactory {
        private Application mApplication;
        public CouponViewModelFactory(Application application) {
            mApplication = application;
        }
        @Override
        public <T extends ViewModel> T create(Class<T> viewModel) {
            return (T) new CouponViewModel(mApplication);
        }
    }
}

Recycler View Adapter PagedListAdapter

To display data in recycler view and interact with PagedList object, we need to create adapter class which extends PagedListAdapter.

You need to implement DiffUtil.ItemCallback and implement areItemsTheSame and areContentsTheSame methods. DiffUtil.ItemCallback object, which is used to compare data objects, is passed to super construction of your adapter class.

Initial data is passed to PagedListAdapter by calling submitList method on it. Internally it handles recycler view scroll events and sends the next page load signals to PagedList by calling loadAround on paged list object.

import android.arch.paging.PagedListAdapter;
import android.support.annotation.NonNull;
import android.support.v7.util.DiffUtil;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;

/** this adapter displays coupon items in recycler view
 *  it extends PagedListAdapter which gets data from PagedList
 *  and displays in recycler view as data is available in PagedList
 */
public class CouponAdapter extends PagedListAdapter<Coupon, CouponViewHolder> {

    protected CouponAdapter() {
        super(DIFF_CALLBACK);
    }

    //DiffUtil is used to find out whether two object in the list are same or not
    public static DiffUtil.ItemCallback<Coupon> DIFF_CALLBACK =
            new DiffUtil.ItemCallback<Coupon>() {
        @Override
        public boolean areItemsTheSame(@NonNull Coupon oldCoupon,
                                       @NonNull Coupon newCoupon) {
            return oldCoupon.get_id() == newCoupon.get_id();
        }

        @Override
        public boolean areContentsTheSame(@NonNull Coupon oldCoupon,
                                          @NonNull Coupon newCoupon) {
            return oldCoupon.equals(newCoupon);
        }
    };

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

    @Override
    public void onBindViewHolder(CouponViewHolder holder, int position) {
        Coupon coupon = getItem(position);
        if(coupon != null) {
            holder.bindTO(coupon);
        }
    }
}

Activity

In the activity, create view model and adapter objects, set recycler view adapter and listen to live data object which exists in view model. In the handler, pass the paged list to adapter by calling the submitList method.

import android.arch.lifecycle.ViewModelProviders;
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;

public class MainActivity extends AppCompatActivity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        RecyclerView recyclerView = findViewById(R.id.coupons_rv);

        CouponViewModel viewModel = ViewModelProviders.of(this,
                new CouponViewModel.CouponViewModelFactory(this.getApplication()))
                .get(CouponViewModel.class);

        CouponAdapter adapter = new CouponAdapter();
        recyclerView.setAdapter(adapter);

        //listen to data changes and pass it to adapter for displaying in recycler view
        viewModel.couponList.observe(this, pagedList -> {
            adapter.submitList(pagedList);
        });

        recyclerView.setLayoutManager(new LinearLayoutManager(this));

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

Other Classes

Following are other classes created for this examples.

RecyclerView ViewHolder

public class CouponViewHolder extends RecyclerView.ViewHolder {
    public TextView storeNameTv;
    public TextView couponTv;

    public CouponViewHolder(View view) {
        super(view);
        storeNameTv = view.findViewById(R.id.coupon_store);
        couponTv = view.findViewById(R.id.coupon_tv);
    }

    public void bindTO(Coupon coupon){
        storeNameTv.setText(coupon.getStore());
        couponTv.setText(coupon.getOffer());
    }
}

Entity

import android.arch.persistence.room.Entity;
import android.arch.persistence.room.PrimaryKey;

@Entity
public class Coupon {
    @PrimaryKey(autoGenerate = true)
    private int _id;
    private String store;
    private String offer;

    public Coupon(){}
    public Coupon(String store, String coupons){
        this.store = store;
        this.offer = coupons;
    }

    public String getStore() {
        return store;
    }

    public void setStore(String store) {
        this.store = store;
    }

    public int get_id() {
        return _id;
    }

    public void set_id(int _id) {
        this._id = _id;
    }

    public String getOffer() {
        return offer;
    }

    public void setOffer(String offer) {
        this.offer = offer;
    }

    @Override
    public boolean equals(Object obj) {
        if (obj == this)
            return true;
        Coupon coupon = (Coupon)obj;
        return coupon.get_id() == this.get_id() &&
                coupon.getStore() == coupon.getStore() &&
                coupon.getOffer() == coupon.getOffer();
    }
}

Room DAO

import android.arch.persistence.room.Dao;
import android.arch.persistence.room.Insert;
import android.arch.persistence.room.Query;

import java.util.List;

@Dao
public interface CouponLocalDAO {
    //to fetch data required to display in each page
    @Query("SELECT * FROM Coupon WHERE  _id >= :id LIMIT :size")
    public List<Coupon> getCouponsBySize(int id, int size);

    //this is used to populate db
    @Insert
    public void insertCoupons(List<Coupon> coupons);
}

Room Database

import android.arch.persistence.room.Database;
import android.arch.persistence.room.RoomDatabase;

@Database(entities = {Coupon.class}, version = 1)
public abstract class CouponsDB extends RoomDatabase {
    public abstract CouponLocalDAO couponDAO();
}

Repository

import android.arch.persistence.db.SupportSQLiteDatabase;
import android.arch.persistence.room.Room;
import android.arch.persistence.room.RoomDatabase;
import android.content.Context;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Executors;

public class LocalRepository {
    private static CouponsDB couponDB;
    private static final Object LOCK = new Object();
    private static Context ctx;

    public synchronized static CouponsDB getCouponDB(Context context) {
        if (couponDB == null) {
            ctx = context;
            synchronized (LOCK) {
                if (couponDB == null) {
                    couponDB = Room.databaseBuilder(context,
                            CouponsDB.class, "Coupons Database")
                            .fallbackToDestructiveMigration()
                            .addCallback(dbCallback).build();

                }
            }
        }
        return couponDB;
    }

    private static RoomDatabase.Callback dbCallback = new RoomDatabase.Callback() {
        public void onCreate(SupportSQLiteDatabase db) {

            Executors.newSingleThreadScheduledExecutor().execute(new Runnable() {
                @Override
                public void run() {
                    addCoupons(ctx);
                }
            });
        }
    };

    private static void addCoupons(Context ctx){
        List<Coupon> couponList = new ArrayList<Coupon>();

        for(String s : initCoupons){
            String[] ss = s.split("\\|");
            couponList.add(new Coupon(ss[0], ss[1]));
        }
        getCouponDB(ctx).couponDAO().insertCoupons(couponList);
    }
    private static String[] initCoupons = {"amazon|falt 20% off on fashion",
            "amazon|upto 30% off on electronics",
            "ebay|falt 20% off on fashion", "ebay|upto 40% off on electronics",
            "nordstorm|falt 30% off on fashion", "bestbuy|upto 80% off on electronics",
            "sears|falt 60% off on fashion", "ee|upto 40% off on electronics",
            "macys|falt 30% off on fashion", "alibaba|upto 90% off on electronics",
            "nordstorm|falt 90% off on fashion", "ebay|upto 40% off on electronics",
            "nordstorm|falt 30% off on fashion", "ebay|upto 70% off on electronics",
            "jcpenny|falt 50% off on fashion", "ebay|upto 50% off on electronics",
            "khols|falt 70% off on fashion", "ebay|upto 40% off on electronics",
            "target|falt 30% off on fashion", "ebay|upto 20% off on electronics",
            "costco|falt 80% off on fashion", "ebay|upto 40% off on electronics",
            "walmart|falt 10% off on fashion", "ebay|upto 10% off on electronics",
            "nordstorm|falt 30% off on fashion", "ebay|upto 70% off on electronics",
            "ebay|falt 40% off on fashion", "ebay|upto 40% off on electronics",
            "nordstorm|falt 70% off on fashion", "ebay|upto 80% off on electronics",
            "nordstorm|falt 30% off on fashion", "ebay|upto 40% off on electronics",
            "nordstorm|falt 60% off on fashion", "ebay|upto 50% off on electronics",
            "ebay|falt 30% off on fashion", "ebay|upto 70% off on electronics",
            "ebay|falt 30% off on fashion", "ebay|upto 40% off on electronics",
            "uuuu|falt 30% off on fashion", "ebay|upto 40% off on electronics",
            "tttt|falt 30% off on fashion", "ebay|upto 40% off on electronics",
            "ssss|falt 30% off on fashion", "ebay|upto 40% off on electronics",
            "eee|falt 30% off on fashion", "ebay|upto 40% off on electronics",
            "www|falt 30% off on fashion", "ebay|upto 40% off on electronics",
            "rrrr|falt 30% off on fashion", "tyyyy|upto 40% off on electronics",
            "vvvv|falt 30% off on fashion", "wwwwe|upto 40% off on electronics",
            "bbbb|falt 30% off on fashion", "ssssssssssssa|upto 40% off on electronics",
            "mmmm|falt 30% off on fashion", "rrtttt|upto 40% off on electronics",

    };
}

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=".MainActivity">
<android.support.v7.widget.RecyclerView
    android:id="@+id/coupons_rv"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
</android.support.v7.widget.RecyclerView>
</LinearLayout>

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_margin="8dp">

    <TextView
        android:id="@+id/coupon_store"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginBottom="8dp"
        android:layout_marginTop="8dp"
        android:textAppearance="@style/TextAppearance.AppCompat.Large"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toLeftOf="@+id/coupon_tv"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintBottom_toBottomOf="parent"/>

    <TextView
        android:id="@+id/coupon_tv"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginBottom="8dp"
        android:layout_marginLeft="16dp"
        android:layout_marginTop="8dp"
        android:textColor="@color/colorAccent"
        android:textAppearance="@style/TextAppearance.AppCompat.Medium"
        app:layout_constraintLeft_toRightOf="@+id/coupon_store"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintBottom_toBottomOf="parent"/>
</android.support.constraint.ConstraintLayout>

ItemKeyedDataSource Example

Following class is an example of ItemKeyedDataSource Which can be used instead of PageKeyedDataSource used in the above example.

public class CouponsItemKeyDataSource extends ItemKeyedDataSource<Integer, Coupon> {
    private CouponLocalDAO couponDAO;

    public CouponsItemKeyDataSource(Context ctx){
        couponDAO = LocalRepository.getCouponDB(ctx).couponDAO();
    }

    @Override
    public void loadInitial(@NonNull LoadInitialParams<Integer> params, @NonNull LoadInitialCallback<Coupon> callback) {
        List<Coupon> cpns = couponDAO.getCouponsBySize(0, params.requestedLoadSize);

        callback.onResult(cpns, 0, cpns.size());
    }

    @Override
    public void loadAfter(@NonNull LoadParams<Integer> params, @NonNull LoadCallback<Coupon> callback) {
        List<Coupon> cpns = couponDAO.getCouponsBySize( params.key, params.requestedLoadSize);
        callback.onResult(cpns);
    }

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

    }

    @NonNull
    @Override
    public Integer getKey(@NonNull Coupon item) {
        return item.get_id();
    }
}