Android & Kotlin

Pagination on Horizontal RecyclerView android

Pinterest LinkedIn Tumblr

This article provides you with a complete solution of pagination of horizontal RecyclerView along with source code. We tried to give our best solution for horizontal RecyclerView pagination. If you feel any improvement please put your comment inside comment box.

Prerequisite

In this demo, I will use RecyclerView, Horizontal SwipeRefreshLayout, Retrofit, Rest API with pagination.

  • RecyclerView – You have to deep knowledge of RecyclerView and how to use it. If not, Read our article on best practices of RecyclerView.
  • HorizontalSwipeRefreshLayout -You should enough understanding of SwipeRefreshLayout. I have created a custom component that works same as SwipeRefreshLayout in horizontal manner. For that read our previous article

Implementation Step of Pagination on Horizontal RecyclerView

  • Create and setup projects
  • Write a POJO class for list item
  • Modify the activity_main.xml and HorizontalSwipeRefreshLayout and RecyclerView
  • For spacing between two items write a space divider
  • Prepare Retrofit client for REST API calls
  • Create a layout file for list item view
  • Writes a RecyclerView adapter for holding recycler item
  • In MainActviity set adapter and RecyclerView and manage pagination on horizontal scroll

Let’s follow above step

Pagination on Horizontal RecyclerView android (Sample APP)

1. Create and setup projects

Let’s move the Android Studio and create a new project with EmptyActivity template. For do that go the file menu and create a new project fill info like name and package name.

2. Write a POJO class for list item

In project main folder create a new model class and create entity such as ID, Main Title, etc.

package com.wave.pagination.recyclerview.model;

import com.google.gson.annotations.SerializedName;

public class NewsItem {

  @SerializedName("created_at")
  private String mCreatedAt;
  @SerializedName("id")
  private String mId;
  @SerializedName("main_title")
  private String mMainTitle;
  @SerializedName("media_url")
  private String mMediaUrl;
  @SerializedName("updated_at")
  private String mUpdatedAt;

  public String getCreatedAt() {
    return mCreatedAt;
  }

  public void setCreatedAt(String createdAt) {
    mCreatedAt = createdAt;
  }

  public String getId() {
    return mId;
  }

  public void setId(String id) {
    mId = id;
  }

  public String getMainTitle() {
    return mMainTitle;
  }

  public void setMainTitle(String mainTitle) {
    mMainTitle = mainTitle;
  }

  public String getMediaUrl() {
    return mMediaUrl;
  }

  public void setMediaUrl(String mediaUrl) {
    mMediaUrl = mediaUrl;
  }

  public String getUpdatedAt() {
    return mUpdatedAt;
  }

  public void setUpdatedAt(String updatedAt) {
    mUpdatedAt = updatedAt;
  }
}

3. Modify the activity_main.xml and HorizontalSwipeRefreshLayout and RecyclerView

Let’s move to next step open activity_main.xml and paste below code. For better understanding of HorizontalSwipeRefreshLayout read here.

<?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"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@drawable/night_sky"
    tools:context=".MainActivity">

    <com.wave.pagination.recyclerview.widget.HorizontalSwipeRefreshLayout
        android:id="@+id/swipeRefresh"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_marginStart="8dp"
        android:layout_marginTop="8dp"
        android:layout_marginEnd="8dp"
        android:layout_marginBottom="8dp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintHorizontal_bias="0.5"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent">

        <android.support.v7.widget.RecyclerView
            android:id="@+id/mRecyclerView"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_marginStart="8dp"
            android:layout_marginTop="8dp"
            android:layout_marginEnd="8dp"
            android:layout_marginBottom="8dp"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent" />
    </com.wave.pagination.recyclerview.widget.HorizontalSwipeRefreshLayout>

</android.support.constraint.ConstraintLayout>

4. For spacing between two items write a space divider

EqualSpacingItemDecoration manages space between two views.

package com.wave.pagination.recyclerview.utils;

import android.graphics.Rect;
import android.support.v7.widget.GridLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.view.View;

public class EqualSpacingItemDecoration extends RecyclerView.ItemDecoration {
    private final int spacing;
    private int displayMode;

    public static final int HORIZONTAL = 0;
    public static final int VERTICAL = 1;
    public static final int GRID = 2;

    public EqualSpacingItemDecoration(int spacing) {
        this(spacing, -1);
    }

    public EqualSpacingItemDecoration(int spacing, int displayMode) {
        this.spacing = spacing;
        this.displayMode = displayMode;
    }

    @Override
    public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
        int position = parent.getChildViewHolder(view).getAdapterPosition();
        int itemCount = state.getItemCount();
        RecyclerView.LayoutManager layoutManager = parent.getLayoutManager();
        setSpacingForDirection(outRect, layoutManager, position, itemCount);
    }

    private void setSpacingForDirection(Rect outRect,
                                        RecyclerView.LayoutManager layoutManager,
                                        int position,
                                        int itemCount) {

        // Resolve display mode automatically
        if (displayMode == -1) {
            displayMode = resolveDisplayMode(layoutManager);
        }

        switch (displayMode) {
            case HORIZONTAL:
                outRect.left = spacing;
                outRect.right = position == itemCount - 1 ? spacing : 0;
                outRect.top = spacing;
                outRect.bottom = spacing;
                break;
            case VERTICAL:
                outRect.left = spacing;
                outRect.right = spacing;
                outRect.top = spacing;
                outRect.bottom = position == itemCount - 1 ? spacing : 0;
                break;
            case GRID:
                if (layoutManager instanceof GridLayoutManager) {
                    GridLayoutManager gridLayoutManager = (GridLayoutManager) layoutManager;
                    int cols = gridLayoutManager.getSpanCount();
                    int rows = itemCount / cols;

                    outRect.left = spacing;
                    outRect.right = position % cols == cols - 1 ? spacing : 0;
                    outRect.top = spacing;
                    outRect.bottom = position / cols == rows - 1 ? spacing : 0;
                }
                break;
        }
    }

    private int resolveDisplayMode(RecyclerView.LayoutManager layoutManager) {
        if (layoutManager instanceof GridLayoutManager) return GRID;
        if (layoutManager.canScrollHorizontally()) return HORIZONTAL;
        return VERTICAL;
    }
}

5. Prepare Retrofit client for REST API calls

5.1 Create Retrofit API Service interface
package com.wave.pagination.recyclerview;

import com.wave.pagination.recyclerview.model.NewsItem;

import java.util.List;
import retrofit2.Call;
import retrofit2.http.GET;
import retrofit2.http.Query;

public interface RestApiService {

  @GET("newsfeed.php")
  Call<List<NewsItem>> getNewsFeed(@Query("page_no") Integer pageNo);
}
5.2 Prepare Retrofit client for API calls
package com.wave.pagination.recyclerview;

import retrofit2.Retrofit;
import retrofit2.converter.gson.GsonConverterFactory;

public class RetrofitInstance {

  private static final String BASE_URL = "https://androidwave.com/api/";
  private static Retrofit retrofit = null;

  public static RestApiService getApiService() {
    if (retrofit == null) {
      retrofit = new Retrofit
          .Builder()
          .baseUrl(BASE_URL)
          .addConverterFactory(GsonConverterFactory.create())
          .build();
    }
    return retrofit.create(RestApiService.class);
  }
}

6. Create a layout file for list item view

6.1 Create a layout file named is list_item for showing the news items.
<?xml version="1.0" encoding="utf-8"?>
<android.support.v7.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:card_view="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="320dp"
    android:layout_height="match_parent"
    android:layout_gravity="center"
    android:layout_marginTop="32dp"
    android:layout_marginBottom="32dp"
    android:background="@color/fui_transparent"
    card_view:cardCornerRadius="10dp"
    card_view:cardElevation="5dp"
    >

  <android.support.constraint.ConstraintLayout
      android:layout_width="match_parent"
      android:layout_height="wrap_content"
      android:layout_gravity="center"
      android:background="#a1ffffff"
      >

    <TextView
        android:id="@+id/txtNewsTitle"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_marginStart="16dp"
        android:layout_marginTop="16dp"
        android:layout_marginEnd="16dp"
        android:gravity="center"
        android:letterSpacing="-0.02"
        android:lineSpacingExtra="8sp"
        android:maxLines="3"
        android:textColor="@color/navy"
        android:textSize="18sp"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintVertical_chainStyle="packed"
        card_view:layout_constraintBottom_toTopOf="@+id/layoutWave"
        card_view:layout_constraintEnd_toEndOf="parent"
        card_view:layout_constraintStart_toStartOf="parent"
        tools:text="What is something that you swear happened but you have no proof and nobody believes you?"
        />

    <android.support.constraint.ConstraintLayout
        android:id="@+id/layoutWave"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_marginStart="16dp"
        android:layout_marginTop="8dp"
        android:layout_marginEnd="16dp"
        android:layout_marginBottom="8dp"
        app:layout_constraintBottom_toTopOf="@+id/txtNewsTitle"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintHeight_default="spread"
        app:layout_constraintHorizontal_bias="0.5"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/txtNewsTitle"
        >

      <ImageView
          android:id="@+id/ivNewsImage"
          android:layout_width="wrap_content"
          android:layout_height="wrap_content"
          android:layout_marginTop="8dp"
          android:layout_marginBottom="8dp"
          android:minHeight="60dp"
          android:scaleType="centerInside"
          android:src="@drawable/picture"
          app:layout_constraintBottom_toBottomOf="parent"
          app:layout_constraintEnd_toEndOf="parent"
          app:layout_constraintStart_toStartOf="parent"
          app:layout_constraintTop_toTopOf="parent"
          />
    </android.support.constraint.ConstraintLayout>
  </android.support.constraint.ConstraintLayout>
</android.support.v7.widget.CardView>
6.2 Prepare view for loading view during API call
<?xml version="1.0" encoding="utf-8"?>
<android.support.v7.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:card_view="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="320dp"
    android:layout_height="match_parent"
    android:layout_gravity="center"
    android:layout_marginTop="32dp"
    android:layout_marginBottom="32dp"
    android:background="@color/fui_transparent"
    card_view:cardCornerRadius="10dp"
    card_view:cardElevation="5dp">

  <android.support.constraint.ConstraintLayout
      android:layout_width="match_parent"
      android:layout_height="match_parent"
      android:layout_gravity="center"
      android:background="#a1ffffff">

    <ProgressBar
        android:id="@+id/progressBar2"
        style="?android:attr/progressBarStyle"
        android:layout_width="56dp"
        android:layout_height="wrap_content"
        android:layout_marginBottom="8dp"
        android:layout_marginEnd="8dp"
        android:layout_marginStart="8dp"
        android:layout_marginTop="8dp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

  </android.support.constraint.ConstraintLayout>
</android.support.v7.widget.CardView>

7. Writes a RecyclerView adapter for holding recycler item

In src folder create a new java class named is PaginationAdapter which extends RecyclerView.Adapter. than paste below code. In this adapter I’m using two views one for loading that will show when api call and second for showing list item

package com.wave.pagination.recyclerview;

import android.support.annotation.NonNull;
import android.support.v7.widget.RecyclerView;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.TextView;
import com.wave.pagination.recyclerview.model.NewsItem;
import java.util.List;

public class PaginationAdapter extends RecyclerView.Adapter<BaseViewHolder> {

  private static final int VIEW_TYPE_LOADING = 0;
  public static final int VIEW_TYPE_NORMAL = 1;
  private boolean isLoaderVisible = false;

  private List<NewsItem> mNewsItemList;
  private Callback mCallback;

  public PaginationAdapter(List<NewsItem> news) {
    this.mNewsItemList = news;
  }

  public void setCallback(Callback callback) {
    mCallback = callback;
  }

  @NonNull
  @Override
  public BaseViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
    switch (viewType) {
      case VIEW_TYPE_NORMAL:
        return new ViewHolder(
            LayoutInflater.from(parent.getContext()).inflate(R.layout.list_item, parent, false));
      case VIEW_TYPE_LOADING:
        return new EmptyViewHolder(
            LayoutInflater.from(parent.getContext()).inflate(R.layout.item_loading, parent, false));
      default:
        return null;
    }
  }

  @Override
  public void onBindViewHolder(@NonNull BaseViewHolder holder, int position) {
    holder.onBind(position);
  }

  @Override
  public int getItemViewType(int position) {
    if (isLoaderVisible) {
      return position == mNewsItemList.size() - 1 ? VIEW_TYPE_LOADING : VIEW_TYPE_NORMAL;
    } else {
      return VIEW_TYPE_NORMAL;
    }
  }

  @Override
  public int getItemCount() {
    return mNewsItemList == null ? 0 : mNewsItemList.size();
  }

  public void add(NewsItem response) {
    mNewsItemList.add(response);
    notifyItemInserted(mNewsItemList.size() - 1);
  }

  public void addAll(List<NewsItem> postItems) {
    for (NewsItem response : postItems) {
      add(response);
    }
  }

  private void remove(NewsItem postItems) {
    int position = mNewsItemList.indexOf(postItems);
    if (position > -1) {
      mNewsItemList.remove(position);
      notifyItemRemoved(position);
    }
  }

  public void addLoading() {
    isLoaderVisible = true;
    add(new NewsItem());
  }

  public void removeLoading() {
    isLoaderVisible = false;
    int position = mNewsItemList.size() - 1;
    NewsItem item = getItem(position);
    if (item != null) {
      mNewsItemList.remove(position);
      notifyItemRemoved(position);
    }
  }

  public void clear() {
    while (getItemCount() > 0) {
      remove(getItem(0));
    }
  }

  NewsItem getItem(int position) {
    return mNewsItemList.get(position);
  }

  public interface Callback {
    void onRepoEmptyViewRetryClick();
  }

  public class ViewHolder extends BaseViewHolder {

    TextView newTitle;
    ImageView newsImage;

    public ViewHolder(View itemView) {
      super(itemView);
      newTitle = itemView.findViewById(R.id.txtNewsTitle);
      newsImage = itemView.findViewById(R.id.ivNewsImage);
    }

    protected void clear() {

    }

    public void onBind(int position) {
      super.onBind(position);

      NewsItem mNewsItem = mNewsItemList.get(position);
      newTitle.setText(mNewsItem.getMainTitle());
/*      Glide.with(itemView.getContext())
          .load(mNewsItem.getMediaUrl())
          .apply(new RequestOptions().override(
              ScreenUtils.getScreenWidth(itemView.getContext()) - ViewUtils.dpToPx(36), 150)
              .placeholder(R.drawable.picture)
              .error(R.drawable.picture))
          .into(newsImage);*/
    }
  }

  public class EmptyViewHolder extends BaseViewHolder {

    public EmptyViewHolder(View itemView) {
      super(itemView);
    }

    @Override
    protected void clear() {

    }
  }
}

8. Create Scroll listener

Create abstract class named PaginationScrollListener. With the help of PaginationScrollListener we will manageapagination

package com.wave.pagination.recyclerview.utils;

import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;

public abstract class PaginationScrollListener extends RecyclerView.OnScrollListener {

  LinearLayoutManager layoutManager;
  private static final int PAGE_SIZE = 10;

  /**
   * Supporting only LinearLayoutManager for now.
   *
   * @param layoutManager
   */
  public PaginationScrollListener(LinearLayoutManager layoutManager) {
    this.layoutManager = layoutManager;
  }

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

  }

  protected abstract void loadMoreItems();

  public abstract boolean isLastPage();

  public abstract boolean isLoading();

}

9. Finally, Open the MainActivity and do following operation

  • Bind the layout component using findViewById
  • Create layout manager instance set orientation is HORIZONTAL
  • Set adapter on RecyclerView
  • Add scroll listener on RecyclerView
  • Set HorizontalSwipeRefreshLayout listener
package com.wave.pagination.recyclerview;

import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.util.Log;

import com.wave.pagination.recyclerview.model.NewsItem;
import com.wave.pagination.recyclerview.utils.EqualSpacingItemDecoration;
import com.wave.pagination.recyclerview.utils.PaginationScrollListener;
import com.wave.pagination.recyclerview.widget.HorizontalSwipeRefreshLayout;

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

import retrofit2.Call;
import retrofit2.Callback;
import retrofit2.Response;

public class MainActivity extends AppCompatActivity implements HorizontalSwipeRefreshLayout.OnRefreshListener {
  private static final String TAG = "MainActivity";

  PaginationAdapter mPaginationAdapter;
  LinearLayoutManager mLayoutManager;

  RecyclerView mRecyclerView;
  HorizontalSwipeRefreshLayout mSwipeRefreshLayout;
  public static final int PAGE_START = 1;
  private int currentPage = PAGE_START;
  private boolean isLastPage = false;
  private int totalPage = 3;
  private boolean isLoading = false;
  int itemCount = 0;


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

    mRecyclerView = findViewById(R.id.mRecyclerView);
    mSwipeRefreshLayout = findViewById(R.id.swipeRefresh);

    mSwipeRefreshLayout.setOnRefreshListener(this);

    mPaginationAdapter = new PaginationAdapter(new ArrayList<NewsItem>());
    mRecyclerView.setHasFixedSize(true);
    mLayoutManager = new LinearLayoutManager(this);
    mLayoutManager.setOrientation(LinearLayoutManager.HORIZONTAL);
    mRecyclerView.setLayoutManager(mLayoutManager);
    mRecyclerView.addItemDecoration(new EqualSpacingItemDecoration(32, EqualSpacingItemDecoration.HORIZONTAL));
    mRecyclerView.setAdapter(mPaginationAdapter);
    mSwipeRefreshLayout.setRefreshing(true);
    preparedListItem();
    /**
     * add scroll listener while user reach in bottom load more will call
     */
    mRecyclerView.addOnScrollListener(new PaginationScrollListener(mLayoutManager) {
      @Override
      protected void loadMoreItems() {
        isLoading = true;
        currentPage++;
        preparedListItem();

      }

      @Override
      public boolean isLastPage() {
        return isLastPage;
      }

      @Override
      public boolean isLoading() {
        return isLoading;
      }
    });
  }


  protected void preparedListItem() {

    RestApiService apiService = RetrofitInstance.getApiService();
    Call<List<NewsItem>> call = apiService.getNewsFeed(currentPage);
    Log.d(TAG, "loadItemFromServer: " + currentPage);
    call.enqueue(new Callback<List<NewsItem>>() {
      @Override
      public void onResponse(Call<List<NewsItem>> call, Response<List<NewsItem>> response) {
        if (currentPage != PAGE_START) mPaginationAdapter.removeLoading();
        mPaginationAdapter.addAll(response.body());
        mSwipeRefreshLayout.setRefreshing(false);
        if (currentPage < totalPage) mPaginationAdapter.addLoading();
        else isLastPage = true;
        isLoading = false;
      }

      @Override
      public void onFailure(Call<List<NewsItem>> call, Throwable t) {
        mSwipeRefreshLayout.setRefreshing(false);
      }
    });
  }

  @Override
  public void onRefresh() {
    itemCount = 0;
    currentPage = PAGE_START;
    isLastPage = false;
    mPaginationAdapter.clear();
    preparedListItem();
  }
}

After following all the above step your app is ready to use, If you have any queries, feel free to ask them in the comment section below. I hope it’s helpful for you, then help me by sharing this post with all your friends who learning android app development. Happy Coding 🙂

Download Source Code

Read More – Pagination in RecyclerView

1 Comment

  1. How to SwipeRefresh from right side.? because we need to come to first position to do SwipeRefresh from left side. I want to do SwipeRefresh from right side when it reaches the last item. Please suggest.

Write A Comment