Pagination with RecyclerView

Almost every API we encounter as developers requires you to handle pagination. When querying an API for some resource instead of delivering all of the results, which could be time consuming and cumbersome to deal with, an API will typically make you to paginate through the results. Of course if you are trying to support pagination on a client you need to handle it gracefully.


RecyclerView

The RecyclerView is a powerful, flexible API which is deemed as the replacement to ListView, GridView, and some other ViewGroups.

It requires you to use the ViewHolder pattern which improves performance as you avoid initializing views every time.

You can decide how you want the items in your adapter to be arranged through the use of LayoutManagers.

LinearLayoutManager — supports both vertical and horizontal lists

StaggeredLayoutManager — supports Pinterest like staggered lists

GridLayoutManager — supports displaying grids as seen in Gallery apps

ItemAnimators are used to provide animations when items are added or removed from an adapter.

If you want to add margins, borders, or dividers between items in the adapter you need to set up an ItemDecorator.


The easiest way to see how all of this gets wired up is with a demonstration. For my example, I am working with the Vimeo API and making a call to the Videos endpoint.

Set up your RecyclerView first. I have chosen a LinearLayoutManger here and set up an ItemAnimator so that items will slide up when added and slide down when removed. Call setAdapter() to wire up the adapter to the RecyclerView. Then don’t forget to add an OnScrollListener.

layoutManager = new LinearLayoutManager(getActivity());
recyclerView.setLayoutManager(layoutManager);
videosAdapter = new VideosAdapter();
recyclerView.setItemAnimator(new SlideInUpAnimator());
recyclerView.setAdapter(videosAdapter);

// Pagination
recyclerView.addOnScrollListener(recyclerViewOnScrollListener);

Let’s see what that OnScrollListener looks like. The responsibility of this listener is to load more items only if some conditions are satisfied. If you aren’t currently loading items and the last page hasn’t been reached then it checks against the current position that is in view to decide whether or not to load more items.

private RecyclerView.OnScrollListener recyclerViewOnScrollListener = new RecyclerView.OnScrollListener() {
@Override
public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
super.onScrollStateChanged(recyclerView, newState);
}

@Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
super.onScrolled(recyclerView, dx, dy);
int visibleItemCount = layoutManager.getChildCount();
int totalItemCount = layoutManager.getItemCount();
int firstVisibleItemPosition = layoutManager.findFirstVisibleItemPosition();

if (!isLoading && !isLastPage) {
if ((visibleItemCount + firstVisibleItemPosition) >= totalItemCount
&& firstVisibleItemPosition >= 0
&& totalItemCount >= PAGE_SIZE) {
loadMoreItems();
}
}
}
};

Ultimately you will be making two different API calls, an API call for the first page and an API call for the next page. Here is what the call to the first page looks like.

Call findVideosCall = vimeoService.findVideos(query,
sortByValue,
sortOrderValue,
currentPage,
PAGE_SIZE);
calls.add(findVideosCall);
findVideosCall.enqueue(findVideosFirstFetchCallback);

And here is the call to the next page.

Call findVideosCall = vimeoService.findVideos(query,
sortByValue,
sortOrderValue,
currentPage,
PAGE_SIZE);
calls.add(findVideosCall);
findVideosCall.enqueue(findVideosNextFetchCallback);

The call to get the first page has a callback method to handle the response. First you add all the videos from the response to the adapter. Then you check if the number of videos is greater than or equal to the PAGE_SIZE. If that is the case then add a loading item to the adapter otherwise set the flag isLastPage to true to prevent loading more items.

private Callback<VideosCollection> findVideosFirstFetchCallback = new Callback<VideosCollection>() {
@Override
public void onResponse(Call<VideosCollection> call, Response<VideosCollection> response) {
loadingImageView.setVisibility(View.GONE);
isLoading = false;

if (!response.isSuccessful()) {
int responseCode = response.code();
if(responseCode == 504) { // 504 Unsatisfiable Request (only-if-cached)
errorTextView.setText("Can't load data.\nCheck your network connection.");
errorLinearLayout.setVisibility(View.VISIBLE);
}
return;
}

VideosCollection videosCollection = response.body();
if (videosCollection != null) {
List<Video> videos = videosCollection.getVideos();
if (videos != null) {
videosAdapter.addAll(videos);

if (videos.size() >= PAGE_SIZE) {
videosAdapter.addFooter();
} else {
isLastPage = true;
}
}
}
}

@Override
public void onFailure(Call<VideosCollection> call, Throwable t) {
NetworkLogUtility.logFailure(call, t);

if (!call.isCanceled()){
isLoading = false;
loadingImageView.setVisibility(View.GONE);

if(t instanceof ConnectException || t instanceof UnknownHostException){
errorTextView.setText("Can't load data.\nCheck your network connection.");
errorLinearLayout.setVisibility(View.VISIBLE);
}
}
}
};

Similarly the call to get the next page has a callback method to handle the response. This time, when the response returns you want to remove the loading item before adding items to the adapter.

private Callback<VideosCollection> findVideosNextFetchCallback = new Callback<VideosCollection>() {
@Override
public void onResponse(Call<VideosCollection> call, Response<VideosCollection> response) {
videosAdapter.removeFooter();
isLoading = false;

if (!response.isSuccessful()) {
int responseCode = response.code();
switch (responseCode){
case 504: // 504 Unsatisfiable Request (only-if-cached)
break;
case 400:
isLastPage = true;
break;
}
return;
}

VideosCollection videosCollection = response.body();
if (videosCollection != null) {
List<Video> videos = videosCollection.getVideos();
if (videos != null) {
videosAdapter.addAll(videos);

if(videos.size() >= PAGE_SIZE){
videosAdapter.addFooter();
} else {
isLastPage = true;
}
}
}
}

@Override
public void onFailure(Call<VideosCollection> call, Throwable t) {
NetworkLogUtility.logFailure(call, t);

if (!call.isCanceled()){
if(t instanceof ConnectException || t instanceof UnknownHostException){
videosAdapter.updateFooter(VideosAdapter.FooterType.ERROR);
}
}
}
};

The complete example of how I set all of this up is in my repo

Loop — https://github.com/lawloretienne/Loop

The files you should focus on are :

Java files

https://github.com/lawloretienne/Loop/blob/master/app/src/main/java/com/etiennelawlor/loop/fragments/VideosFragment.java

https://github.com/lawloretienne/Loop/blob/master/app/src/main/java/com/etiennelawlor/loop/adapters/VideosAdapter.java

Xml files

https://github.com/lawloretienne/Loop/blob/master/app/src/main/res/layout/fragment_videos.xml

https://github.com/lawloretienne/Loop/blob/master/app/src/main/res/layout/video_row.xml