https://medium.com/@shashankmohabia/android-paging-efficient-way-to-populate-recycler-view-dynamically-31f39f35cdf9

처음 개념 잡기 쉽게 자세히 설명이 되어 있다.

Paging Library Architecture:

image

The paging library consists of 5 components:

  • PagedList — The Paging Library’s key component is the PagedList class, which is a collection that loads chunks of your app’s data, or pages, asynchronously. It can be used to load data from sources you define, and present it easily in your UI with a RecyclerView. As more data is needed, it’s paged into the existing PagedList object. If any loaded data changes, a new instance of PagedList is emitted to the observable data holder from a LiveData or RxJava2-based object. As PagedList objects are generated, your app’s UI presents their contents, all while respecting your UI controllers’ lifecycles.
  • DataSource and DataSource.Factory — a DataSource is the base class for loading snapshots of data into a PagedList. A DataSource.Factory is responsible for creating a DataSource.
  • LivePagedListBuilder — builds a LiveData<PagedList>, based on DataSource.Factory and a PagedList.Config(the configuration file for paging).
  • BoundaryCallback — signals when a PagedList has reached the end of available data so that more data can be fetched from the server in case of DB backed up by the server.
  • PagedListAdapter — a RecyclerView.Adapter that presents paged data from PagedLists in a RecyclerView. PagedListAdapter listens to PagedList loading callbacks as pages are loaded and uses DiffUtil to compute fine-grained updates as new PagedLists are received.

Implementation:

Step 1. Load data in chunks with the PagedList:

To start using paging, replace all the List data to PagedList in the ViewModels and its observers in activities and fragments. If there are any models responsible for updating UI, change them also.

val dataList: LiveData<List<data_model_object>> = ...
val dataList: LiveData<PagedList<data_model_object>> = ...
viewModel.dataList.observe(this, Observer<PagedList<data_model_object>> {...})

Step 2. Define the source of data for the paged list:

The PagedList loads content dynamically from a source. If the database is the main source of truth for the UI, it also represents the source for the PagedList. If your app gets data directly from the network and displays it without caching, then the class that makes network requests would be your data source.

A source is defined by a DataSource class. To page in data from a source that can change—such as a source that allows inserting, deleting or updating data—you will also need to implement a DataSource.Factory that knows how to create the DataSource. Whenever the data is updated, the DataSource is invalidated and re-created automatically through the DataSource.Factory.

The Room persistence library provides native support for data sources associated with the Paging library. For a given query, Room allows you to return a DataSource.Factory from the DAO and handles the implementation of the DataSource for you.

@Daointerface RepoDao {
   @Query(<query>)
   fun someQueryMethod(): LiveData<List<data_model_object>>
   fun someQueryMethod(): DataSource.Factory<Int, data_model_object>...}

Step 3. Build and Config the paged list:

To build and configure a LiveData<PagedList>, use a LivePagedListBuilder.

//get the dataSourceFactory by making a call to dao function
val data = LivePagedListBuilder(dataSourceFactory, DATABASE_PAGE_SIZE).build()

Besides the DataSource.Factory, you need to provide a PagedList configuration, which can include the following options:

  • The size of a page loaded by a PagedList
  • How far ahead to load
  • How many items to load when the first load occurs during the creation of pagedList by DataSource.
  • Whether null items can be added to the PagedList, to display placeholders for data that hasn’t been loaded yet. As soon as the data becomes available the ViewHolder gets populated with that and the view is updated with a nice cross-fade animation. If you have it disabled then your scroll bar jumps whenever data loads and that gives rise to bad user experience.
    But there are some problems with placeholders:
    a) All the items should be of the same size otherwise the animation looks weird.
    b) The adapter must handle null items.
    c) DataSource must count items (room does that implicitly).

You can have a separate config object to add configurations.

val config = PagedList.Config.Builder()
.setPageSize(30) //mandatory to provide
.setInitialLoadSizeHint(50) //default: page size*3
.setPrefetchDistance(10) //default: page size
.setEnablePlaceholder(true/false) //default: true
.build()val data = LivePagedListBuilder(dataSourceFactory, config).build()

Note: The DataSource page size should be several screens’ worth of items. If the page is too small, your list might flicker as pages content doesn’t cover the full screen. Larger page sizes are good for loading efficiency, but can increase latency when the list is updated.

Step 4. Make the RecyclerView Adapter work with a PagedList:

To bind a PagedList to a RecycleView, use a PagedListAdapter. The PagedListAdapter gets notified whenever the PagedList content is loaded and then signals the RecyclerView to update.

class RecyclerViewAdapter : PagedListAdapter<data_model_object, RecyclerView.ViewHolder>(DATA_COMPARATOR)

Also notice that now the data item is nullable and we have to update the onBindViewHolder to make it nullable. You can work with that data by applying checks if you want.

val item:data_model_object? = getItem(position)// optional        
if (item != null) {            
(holder as itemViewHolder).bind(item)        }

Step 5. Trigger network updates:

If your entire data is on a local database then we are done and you do not need to perform this step. But if your local DB is backed up by a server then we should find to way to ask for more data from the server only when there is no more data in the local DB. By providing all the data to the UI from the DB itself we follow the single source of truth principle.

For this, we use BoundaryCallback which gives two methods to perform some task when there are zero items in the list or the last available has been loaded so we need more data.

For this create a class extending the BoundaryCallback class and provide it the service and the local DB instances. Then override the methods to update the DB with your server data.

class sampleBoundaryCallback(
       private val service: ApiService,
       private val db: localDb,
       private val query:String
) : PagedList.BoundaryCallback<data_model_object>() {
   override fun onZeroItemsLoaded() {
        loadDataFromServerAndStoreInDB()}

   override fun onItemAtEndLoaded(itemAtEnd: data_model_object) {
        loadDataFromServerAndStoreInDB()    }
}

Then add the callback to LivePagedListBuilder:

val boundaryCallback = SampleBoundaryCallback(service, db, query)

   // Get the paged list
   val data = LivePagedListBuilder(dataSourceFactory, config)
            .setBoundaryCallback(boundaryCallback)
            .build()

Wrap up:

Now that we added all the components, let’s take a step back and see how everything works together.

The DataSource.Factory (implemented by Room) creates the DataSource. Then, LivePagedListBuilder builds the LiveData<PagedList>, using the passed-in DataSource.Factory, BoundaryCallback, and PagedList configuration. This LivePagedListBuilder object is responsible for creating PagedList objects. When a PagedList is created, two things happen at the same time:

  • The LiveData emits the new PagedList to the ViewModel, which in turn passes it to the UI. The UI observes the changed PagedList and uses its PagedListAdapter to update the RecyclerView that presents the PagedList data. (The PagedList is represented in the following animation by an empty square).
  • The PagedList tries to get the first chunk of data from the DataSource. When the DataSource is empty, for example when the app is started for the first time and the database is empty, it calls BoundaryCallback.onZeroItemsLoaded(). In this method, the BoundaryCallback requests more data from the network and inserts the response data in the database.
image

After the data is inserted in the DataSource, a new PagedList object is created (represented in the following animation by a filled-in square). This new data object is then passed to the ViewModel and UI using LiveData and displayed with the help of the PagedListAdapter.

image

When the user scrolls, the PagedList requests that the DataSource load more data, querying the database for the next chunk of data. When the PagedList paged all the available data from the DataSource, BoundaryCallback.onItemAtEndLoaded() is called. The BoundaryCallback requests data from the network and inserts the response data in the database. The UI then gets re-populated based on the newly-loaded data.

image

You can find a sample project implementing Paging here. I will also make a video tutorial for paging on youtube and will add the link here.

Feel free to comment if there are doubts or if you feel anything needs to be corrected😀.

Resources:

IO18’ Paging library launch

Android Codelab

Android Developers documentation

.

.

.

.

.

https://www.zoftino.com/pagination-in-android-using-paging-library

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  (PagedList)  which acts as mediator between UI and Database uses data source to asynchronously fetch page data and populates itself with the loaded data.  (PagedList 는 UI와 데이터베이스 사이에 위치하며 data source를 이용하여 데이터를 가져온다. 새로 가져온 데이터를 가지고 PagedList 가 생성된다. )  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.  (PagedList 는 page관련 config정보를 가지며 page관련 정보도 관리한다)  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.  (UI에서 더이상 보여줄 데이터가 없어진 경우 이를 PagedList 알리고 추가로 데이터를 가져오게 한다 ) 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   (이를 통해 PagedList를 만든다 ). LivePagedListBuilder is supplied with data source factory and page list configuration objects.   (LivePagedListBuilder 만들때 전달해야 하는 parameters로는 data source factory, pagedlist의 config내용이 있다)

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.    (PagedList는 livedata형태로 adapater에 전달된다)   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.   (adapter는 다음 page를 위한 신호를 PagedList에 보내고 data update를 기대한다)

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.   (데이터가 업데이트된 경우 PagedList는 데이터를 reload하는것이 아니고 새로운 PagedList를 생성한다 )

DataSource.Factory    which is passed to    LivePagedListBuilder   is a factory for data source.    (DataSource.Factory는  data source를 생성하는 역할을 하며  LivePagedListBuilder에 전달된다)   You need to create DataSource.Factory and override create() method which returns DataSource object.   (DataSource.Factory의 create()를 override하고 이것의 리턴값이 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.    (DataSource는 PagedList에 의해 데이터를 load하는 작업을 수행하며 PageKeyedDataSource, ItemKeyedDataSource, PositionalDataSource 중 하나를 extend한다)   You need to implement loadInitial and loadAfter methods. Using parameters in loadInitial and loadAfter methods, you can prepare query and load data.   (DataSource는 loadInitial, loadAfter 두 메소드를 implement한다 )

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.   (PagedListAdapter 는 다음페이지를 위해 PagedList object의 loadAround를 호출한다)

.

.

.

.

.

https://youtu.be/c_v0y2k-bww

10분까지 내용 개념 성명 좋음

image
image
image
image
image
image
image
image
image

.

.

.

.

.

실제 예제 코드

orginal source : https://www.zoftino.com/pagination-in-android-using-paging-library

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();
    }
}