Skip to content

Paging Library Guide

Roger Hu edited this page May 25, 2018 · 31 revisions

Paging Library Guide

A common feature is to load data automatically as a user scrolls through the items (i.e. infinite scroll). Previously there were only third party options that could accomplish this goal. Google's new Paging Library now provides this support.

Setup

Make sure to add the Google Maven repository in your root build.gradle:

allprojects {
  repositories {
    google()
    jcenter()
  }
}

Next, add the Paging library to your app/build.gradle:

dependencies {
     implementation "android.arch.paging:runtime:1.0.0"
}

Modifying your RecyclerView

The Paging Library can only be used with RecyclerView. If you are using ListView to display your lists, you should first migrate to using the ViewHolder pattern before transitioning to RecyclerView. Moving to RecyclerView may also require changing how click handling is performed.

Modifying to ListAdapter

Once your lists are using RecyclerView, the next step is to change your adapters to inherit from the ListAdapter class instead of the RecyclerView.Adapter class. See this guide for more information.

Moving to PagedListAdapter

Once the ListAdapter is made, we will modify the ListAdapter to be a PagedListAdapter. The PagedListAdapter enables for data to be loaded in chunks and does not require all the data to be loaded in memory.

Change from:

public class TweetAdapter extends RecyclerView.Adapter<PostAdapter.ViewHolder> {

To:

public class TweetAdapter extends ListAdapter<Post, PostAdapter.ViewHolder> {

In addition, the adapter should no longer retain a copy of its current list. The getItem() method should be used instead.

override fun onBindViewHolder(holder: PostAdapter.ViewHolder, position: Int) {

    // getItem() should be used with ListAdapter
    val tweet = getItem(position)

    // null placeholders if the PagedList is configured to use them
    // only works for data sets that have total count provided (i.e. PositionalDataSource)
    if (tweet == null)
    {
        return;
    }
    // handle remaining work here
    // ..
}

Add a data source

The next step is to define our data sources. If you are already making existing network or database calls somewhere else, you most likely will need to move this code into a data source. You will also need to first pick what kind of data source to use:

  • ItemKeyedDataSource - provides some type of key to use to fetch the next data size and unknown list size (example: Twitter API)
  • PageKeyedDataSource - provides a next/before link and has unknown list size (example: Facebook Graph API)
  • PositionalDataSource - Useful for sources that provide a fixed size list with known PositionalDataSource (example: Room, Parse)
Using ItemKeyedDataSource

The first step is to define the key that will be used to determine the next page of data. In the Twitter API case, for instance, the Twitter Post ID can be used as a way to query for posts older than that Tweet ID. The last post will represent the next set of posts to retrieve:

public class TweetDataSource extends ItemKeyedDataSource<Long, Tweet> {

  // first type of ItemKeyedDataSource should match return type of getKey()
  @NonNull
  @Override
  public Long getKey(@NonNull Tweet item) {
      return item.getPostId();
  }

Next, we should make sure to pass into the data constructor whatever dependencies are needed.

// pass whatever dependencies are needed to make the network call
TwitterClient mClient;

// define the type of data that will be emitted by this datasource
public TweetDataSource(TwitterClient client) {
    mClient = client;
    tweetsLiveData = new MutableLiveData<>();
}

Next, we need to define inside the data source the loadInitial() and loadAfter().

public void loadInitial(@NonNull LoadInitialParams<Long> params, @NonNull final LoadInitialCallback<Tweet> callback) {

   JsonHttpResponseHandler jsonHttpResponseHandler = createTweetHandler(callback);
   // no max_id should be passed on initial load
   mClient.getHomeTimeline(params.requestedLoadSize, jsonHttpResponseHandler);
}

// Called repeatedly when more data needs to be set
@Override
public void loadAfter(@NonNull LoadParams<Long> params, @NonNull LoadCallback<Tweet> callback) {
   // fetch data synchronously
   JsonHttpResponseHandler jsonHttpResponseHandler = createTweetHandler(callback);

   // params.key & requestedLoadSize should be used
   // params.key should be used for the max_id= parameter in Twitter API.
   mClient.getHomeTimeline(params.key, params.requestedLoadSize, jsonHttpResponseHandler);
}

The createTweetHandler() method parses the JSON data and posts the data to the RecyclerView adapter and PagedList handler. Keep in mind that all of these network calls are already done in a background thread, so the network call should be forced to run synchronously:

public JsonHttpResponseHandler createTweetHandler(final LoadCallback<Tweet> callback) {

 JsonHttpResponseHandler handler = new JsonHttpResponseHandler() {
     @Override
     public void onSuccess(int statusCode, Header[] headers, JSONArray response) {
         ArrayList<Tweet> tweets = new ArrayList<Tweet>();
         tweets = Tweet.fromJson(response);

         // send back to PagedList handler           
         callback.onResult(tweets);
     }
 };

 // fetch data synchronously
 // For AsyncHttpClient, this workaround forces the callback to be run synchronously
 handler.setUseSynchronousMode(true);
 handler.setUsePoolThread(true);

Add a Data Source Factory

A data source factory simply creates the data source:

public class TweetDataSourceFactory extends DataSource.Factory<Long, Tweet> {
    MutableLiveData<TweetDataSource> tweetDataSource;
    TwitterClient client;

    public TweetDataSourceFactory(TwitterClient client) {
        this.client = client;
        this.tweetDataSource = new MutableLiveData<>();
    }
    @Override
    public DataSource<Long, Tweet> create() {
        TweetDataSource dataSource = new TweetDataSource(this.client);
        tweetDataSource.postValue(dataSource);
        return dataSource;
    }
}

Creating a PagedList

Once we have created the data source and data source factory, the final step is to create the PagedList and listen for updates. We can then call the submitList() on our adapter with this next set of data:

public abstract class BaseTimelineFragment extends Fragment {

  LiveData<PagedList<Tweet>> tweets;

  public void onActivityCreated(Bundle savedInstanceState) {

    tweets = new LivePagedListBuilder(factory, config).build();

    tweets.observe(this, new Observer<PagedList<Tweet>>() {
			    @Override
			    public void onChanged(@Nullable PagedList<Tweet> tweets) {
				tweetAdapter.submitList(tweets);
			    }
		  });

  }

Adding multiple data sources

The Paging Library requires that the data used in the RecyclerView, regardless of whether they are pulled from the network, database, or memory, be defined as data sources. It also allows the flexibility to first pull data from the database or memory, and if there is no more data, to pull additional segments of data from the network. To help showcase how the Paging Library works, you can also try out this sample code from Google.

References

Finding these guides helpful?

We need help from the broader community to improve these guides, add new topics and keep the topics up-to-date. See our contribution guidelines here and our topic issues list for great ways to help out.

Check these same guides through our standalone viewer for a better browsing experience and an improved search. Follow us on twitter @codepath for access to more useful Android development resources.

Clone this wiki locally