API endpoints that return a list of data often have pagination parameters that you can pass in to get different pages of lists back. Often, they have something like page and per_page parameters that you can pass in.
Let’s say that there’s an API endpoint that returns a list of books. In the database of the API, there are 100,000 books. It would be impractical to fetch all 100,000 books from the API all in one request. In fact, we would probably get an OutOfMemory exception. To avoid this, we want to paginate through our list of books when making requests to our API. Just as a note, the code samples below for making the API calls are using Retrofit with GSON.
Setting up Retrofit for pagination
Here’s our Retrofit interface with a definition that will get a list of books with pagination
1 2 3 4 |
public interface BookLibraryApi { @GET("books") Call<List<Book>> getBooks(@Query("page") int page, @Query("limit") int limit); } |
Here, the page is referring to the page number that you want and limit is referring to the number of books you would like to fetch per page.
I generally like to have an interface where I define variables that will be constants throughout my application. Page limit for pagination purposes when making a API request is one of those things that I like to keep consistent throughout my apps, unless it’s a requirement that they be different throughout the app. So let’s make a Constants
interface.
1 2 3 4 |
public interface Constants { STRING BASE_URL = "https://api.somebookapi.com/"; int PAGE_LIMIT = 20; } |
Here’s I have set up a Constants
interface that I can implement throughout my activities and fragments. I also defined a URL_DOMAIN that would be consistent throughout the app.
Now, let’s write our class that will generate our RetrofitService
.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
package com.grasspass.api; import okhttp3.OkHttpClient; import okhttp3.logging.HttpLoggingInterceptor; import retrofit2.Retrofit; import retrofit2.converter.gson.GsonConverterFactory; public class ServiceGenerator implements Constants { private static Retrofit.Builder builder = new Retrofit.Builder() .baseUrl(BASE_URL) .addConverterFactory(GsonConverterFactory.create()); private static Retrofit retrofit = builder.build(); private static HttpLoggingInterceptor logging = new HttpLoggingInterceptor().setLevel(HttpLoggingInterceptor.Level.BODY); private static OkHttpClient.Builder httpClient = new OkHttpClient.Builder(); public static <S> S createService(Class<S> serviceClass) { if (!httpClient.interceptors().contains(logging)) { httpClient.addInterceptor(logging); builder.client(httpClient.build()); retrofit = builder.build(); } return retrofit.create(serviceClass); } } |
This ServiceGenerator
just has a factory method that will generate our service whenever we need it.
Making the API call
We’ll be making the API requests in our MainActivity
. Let’s also say that we have already defined our activity_main.xml
with a RecyclerView
and that we have our layout for our row adapters for our RecyclerView
all set and ready to go.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
public class MainActivity extends AppCompatActivity implements Constants { private static final String LOG_TAG = MainActivity.class.getSimpleName(); private List<Book> mBooks; public static void startActivity(Activity startingActivity) { startingActivity.startActivity(new Intent(startingActivity, MainActivity.class)); } @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); BookLibraryApiService service = ServiceGenerator.createService(BookLibraryApiService.class); Call<List<Book>> call = service.getBooks(1, PAGE_LIMIT); call.enqueue(new Callback<List<Book>> { @Override public void onResponse(Call<List<Book>> call, Response<List<Book>> response) { if (response.isSuccess()) { mBooks = response.body(); else { Log.d(LOG_TAG, "Request failed: " + response.getMessage()); } } }); } public class RowAdapter extends RecyclerView.Adapter<RowAdapter.ViewHolder> { // Your typical average RecyclerView code with ViewHolder } } |
Building the pagination
We’ll be using a library for this portion. There are two main reasons for this.
- It makes for shorter, more readable, and concise code
- It comes with Material Design loader all ready and set to go
- Most guides that shows you how to do this manually don’t work very well
The biggest reason is number 3. If you do a quick Google search on how to paginate through a RecyclerView
, you’ll quickly realize that there isn’t really a consensus on how to do this properly. Also, there isn’t really a clean API on the RecyclerView
that allows you to detect the exact position of the user in your RecyclerView. Just to figure out where your user is, you have to do all sorts of positioning math wizardry, which can take time to figure out and can be easy to get wrong. Thus, to make our lives easier, we use a library.
The one that I found to be the simplest and straightforward to use is the Paginate library by MarkoMilos (https://github.com/MarkoMilos/Paginate). So add that to your app/build.gradle
file and also add the SmoothProgressBar (https://github.com/castorflex/SmoothProgressBar) to it too. The SmoothProgressBar is actually already a dependency of the Paginate library, but in case you want to customize your loader spinner that the user sees when he/she reaches the bottom of the page, you’ll need that SmoothProgressBar library added separately to your app/build.gradle
file.
Finally, let’s set up our pagination!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 |
public class MainActivity extends AppCompatActivity implements Constants, Paginate.Callbacks { private static final String LOG_TAG = MainActivity.class.getSimpleName(); private List<Book> mBooks; private RowAdapter mRowAdapter = new RowAdapter(); private boolean mLoading = false; private int mCurrentPage = 1; private BookLibraryApiService mService; private RecyclerView mRecyclerView; private Paginate mPaginate; public static void startActivity(Activity startingActivity) { startingActivity.startActivity(new Intent(startingActivity, MainActivity.class)); } @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); mService = ServiceGenerator.createService(BookLibraryApiService.class); mRecyclerView = (RecyclerView) findViewBy(R.id.recycler); mPaginate = Paginate.with(mRecycler, this) .setLoadingTrigger(10) .addLoadingListItem(true) .setLoadingListItemCreator(null) .setLoadingListItemSpanSizeLookup(new LoadingListItemSpanLookup() { @Override public int getSpanSize() { return 3; } }) .build(); loadBooks(); } private void loadBooks() { Call<List<Book>> call = mService.getBooks(mCurrentPage, PAGE_LIMIT); call.enqueue(new Callback<List<Book>>) { @Override public void onResponse(Call<List<Book>> call, Response<List<Book>> response) { if (response.isSuccess()) { mBooks = response.body(); mRowAdapter.addBooks(mBooks); mRowAdapter.notifyDataSetChanged(); mLoading = false; } else { Log.d(LOG_TAG, "Request failed: " + response.getMessage()); } } } } @Override public void onLoadMore() { mLoading = true; if (mBooks.hasNextPage) { mCurrentPage += 1; loadBooks(); } } @Override public boolean isLoading() { return mLoading; } @Override public boolean hasLoadedAllItems() { // our BooksAPI has a boolean field that tells us whether our response has a next page or not return !mBooks.hasNextPage; } public class RowAdapter extends RecyclerView.Adapter<RowAdapter.ViewHolder> { // Your typical average RecyclerView code with ViewHolder } } |
A few things have been added and changed in this final version.
First major thing is that we have added the overriding callbacks that are required by the Paginate
library. As for the instance variables, we have added three:
- Boolean variable mLoading which is initialized to false and
- Integer variable mCurrentPage which is initialized to 1
- BookLibraryServiceAPI mService so that we can initialize it just once and reuse throughout the MainActivity class
- There is a new variable mPaginate that will initialize the pagination feature.
The mLoading
variable is set to true when we’re making the API call, but is set back to false when the API call has finished. We then use this variable in the overridden isLoading()
method to tell our Paginate
library whether new list is being loaded into our RecyclerView or not.
The mCurrentPage
variable is incremented by 1 in the onLoadMore()
method so that whenever we are loading a new set of books, we are retrieving the next page.
When we piece all of this together, you’ll see the beautiful pagination working whenever the user scrolls down.
A couple of notes on the mPaginate build method and its chained methods and parameters (the important parts).
- In the Paginate.with(), you pass in the RecyclerView that you are paginating by
- The setLoadingTriggerThreshold is the number of items that are left in the RecyclerView from the bottom of the list while the user is scrolling that should trigger the next API request. In my example, I have set this to 10. So, as the user scrolls, as soon as there are 10 items left in the RecyclerView, the next API request (if there are any more pages left) will be made.
And that’s it! This should allow you to build in smooth pagination features in your apps.