Almost every REST API your app call requires to handle pagination. When calling a REST API for resource delivering all of the results could be time-consuming, a REST API will typically make you paginate through the results. If you are trying to support pagination on a client you need to handle it gracefully.
Let’s say that there’s an API endpoint that returns a list of users. In the database of the API, there are 100,000+ users. It would be impractical to fetch all 100,000 users 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 the user when making requests to our API.
Paging Library
The new paging library makes it easier for your app to gradually load information as needed from a REST API, without overloading the device or waiting too long for an all result.This library contains several classes to streamline the process of requesting data as you need it. These classes also work seamlessly with existing architecture components, like Room.
1.Adding Components to your Project
Architecture Components are available from Google’s Maven repository. To use them, follow these steps: Open the build.gradle
file for your project and add the line as shown below:
allprojects { repositories { jcenter() maven { url 'https://maven.google.com' } } }
Add Architecture Components
In this tutorial, we are using LiveData, and ViewModel. Open the build.gradle
file for your app or module and add the artifacts that you need as dependencies:
dependencies { .... // ViewModel and LiveData implementation 'androidx.lifecycle:lifecycle-extensions:2.0.0-beta01' annotationProcessor "androidx.lifecycle:lifecycle-compiler:2.0.0-beta01" // Room implementation 'androidx.room:room-runtime:2.0.0-beta01' annotationProcessor "androidx.room:room-compiler:2.0.0-beta01" // Paging implementation 'androidx.paging:paging-runtime:2.0.0-beta01' implementation 'com.squareup.retrofit2:retrofit:2.4.0' implementation 'com.squareup.retrofit2:converter-gson:2.4.0' implementation 'androidx.recyclerview:recyclerview:1.0.0-beta01' implementation 'androidx.appcompat:appcompat:1.0.0-beta01' }
2.Setting up Retrofit for Pagination
The code samples below for making the API calls are using Retrofit with GSON.We are working with the GitHub API and making a call to the GitHub User endpoint.
public interface GitHubService { @GET("/users") Call<List<User>> getUser(@Query("since") int since, @Query("per_page") int perPage); }
Now, let’s write our class that will generate our RetrofitService.
public class GitHubApi { public static GitHubService createGitHubService() { Retrofit.Builder builder = new Retrofit.Builder() .addConverterFactory(GsonConverterFactory.create()) .baseUrl("https://api.github.com"); return builder.build().create(GitHubService.class); } }
3.Create ItemKeyedDataSource
Use ItemKeyedDataSource
class to define a data source.It uses for incremental data loading for paging keyed content, where loaded content uses previously loaded items as input to next loads. Read more about different DataSource type use case. Implement a DataSource using ItemKeyedDataSource
if you need to use data from item N - 1
to load item N
. This is common, for example, in sorted database queries where attributes of the item such just before the next query define how to execute it. To implement, extend ItemKeyedDataSource
subclass.
//A data source that uses the "userId" field of User as the key for next/prev user list. public class ItemKeyedUserDataSource extends ItemKeyedDataSource<Long, User> { public static final String TAG = "ItemKeyedUserDataSource"; GitHubService gitHubService; LoadInitialParams<Long> initialParams; LoadParams<Long> afterParams; private MutableLiveData networkState; private MutableLiveData initialLoading; private Executor retryExecutor; public ItemKeyedUserDataSource(Executor retryExecutor) { gitHubService = GitHubApi.createGitHubService(); networkState = new MutableLiveData(); initialLoading = new MutableLiveData(); this.retryExecutor = retryExecutor; } public MutableLiveData getNetworkState() { return networkState; } public MutableLiveData getInitialLoading() { return initialLoading; } @Override public void loadInitial(@NonNull LoadInitialParams<Long> params, @NonNull LoadInitialCallback<User> callback) { Log.i(TAG, "Loading Rang " + 1 + " Count " + params.requestedLoadSize); List<User> gitHubUser = new ArrayList(); initialParams = params; initialLoading.postValue(NetworkState.LOADING); networkState.postValue(NetworkState.LOADING); gitHubService.getUser(1, params.requestedLoadSize).enqueue(new Callback<List<User>>() { @Override public void onResponse(Call<List<User>> call, Response<List<User>> response) { if (response.isSuccessful() && response.code() == 200) { gitHubUser.addAll(response.body()); callback.onResult(gitHubUser); initialLoading.postValue(NetworkState.LOADED); networkState.postValue(NetworkState.LOADED); initialParams = null; } else { Log.e("API CALL", response.message()); initialLoading.postValue(new NetworkState(Status.FAILED, response.message())); networkState.postValue(new NetworkState(Status.FAILED, response.message())); } } @Override public void onFailure(Call<List<User>> call, Throwable t) { String errorMessage; errorMessage = t.getMessage(); if (t == null) { errorMessage = "unknown error"; } networkState.postValue(new NetworkState(Status.FAILED, errorMessage)); } }); } @Override public void loadAfter(@NonNull LoadParams<Long> params, @NonNull LoadCallback<User> callback) { Log.i(TAG, "Loading Rang " + params.key + " Count " + params.requestedLoadSize); List<User> gitHubUser = new ArrayList(); afterParams = params; networkState.postValue(NetworkState.LOADING); gitHubService.getUser(params.key, params.requestedLoadSize).enqueue(new Callback<List<User>>() { @Override public void onResponse(Call<List<User>> call, Response<List<User>> response) { if (response.isSuccessful()) { gitHubUser.addAll(response.body()); callback.onResult(gitHubUser); networkState.postValue(NetworkState.LOADED); afterParams = null; } else { networkState.postValue(new NetworkState(Status.FAILED, response.message())); Log.e("API CALL", response.message()); } } @Override public void onFailure(Call<List<User>> call, Throwable t) { String errorMessage; errorMessage = t.getMessage(); if (t == null) { errorMessage = "unknown error"; } networkState.postValue(new NetworkState(Status.FAILED, errorMessage)); } }); } @Override public void loadBefore(@NonNull LoadParams<Long> params, @NonNull LoadCallback<User> callback) { } @NonNull @Override public Long getKey(@NonNull User item) { return item.userId; } }
4.DataSourceFactory
A simple data source factory which also provides a way to observe the last created data source. This allows us to channel its network request status etc back to the UI.
public class GitHubUserDataSourceFactory implements DataSource.Factory { MutableLiveData<ItemKeyedUserDataSource> mutableLiveData; ItemKeyedUserDataSource itemKeyedUserDataSource; Executor executor; public GitHubUserDataSourceFactory(Executor executor) { this.mutableLiveData = new MutableLiveData<ItemKeyedUserDataSource>(); this.executor = executor; } @Override public DataSource create() { itemKeyedUserDataSource = new ItemKeyedUserDataSource(executor); mutableLiveData.postValue(itemKeyedUserDataSource); return itemKeyedUserDataSource; } public MutableLiveData<ItemKeyedUserDataSource> getMutableLiveData() { return mutableLiveData; } }
5.Create ViewModel
In the ViewModel, we would extend from the Architecture Component ViewModel
, and then we would keep a reference to that LiveData
of our PagedList
.
public class UserViewModel extends ViewModel { public LiveData<PagedList<User>> userList; public LiveData<NetworkState> networkState; Executor executor; LiveData<ItemKeyedUserDataSource> tDataSource; public UserViewModel() { GitHubUserDataSourceFactory githubUserDataSourceFacteory = new GitHubUserDataSourceFactory(executor); tDataSource = githubUserDataSourceFacteory.getMutableLiveData(); networkState = Transformations.switchMap(githubUserDataSourceFacteory.getMutableLiveData(), dataSource -> { return dataSource.getNetworkState(); }); PagedList.Config pagedListConfig = (new PagedList.Config.Builder()).setEnablePlaceholders(false) .setInitialLoadSizeHint(10) .setPageSize(20).build(); userList = (new LivePagedListBuilder(githubUserDataSourceFacteory, pagedListConfig)) .build(); } }
LivePagedListBuilder:This class generates a LiveData
<
PagedList
>
from the DataSource.Factory
you provide. PagedList: A PagedList
is a List which loads its data in chunks (pages) from a DataSource
.All data in a PagedList
is loaded from its DataSource
. Creating a PagedList
loads data from the DataSource
immediately, and should, for this reason, be done on a background thread. The constructed PagedList
may then be passed to and used on the UI thread. This is done to prevent passing a list with no loaded content to the UI thread, which should generally not be presented to the user.
6.Create ProgressBar as Footer in a RecyclerView
Create RecyclerView with 2 type of items one is our usual items the second is a progress bar, then we need to listen NetworkState LiveData and decide are we going to show ProgressBar or not. network_state_item.xml
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical" android:padding="8dp"> <TextView android:id="@+id/error_msg" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center_horizontal"/> <ProgressBar android:id="@+id/progress_bar" style="?android:attr/progressBarStyle" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_gravity="center"/> </LinearLayout>
Now, set the Network State and add or remove ProgressBar in Adapter.
public void setNetworkState(NetworkState newNetworkState) { NetworkState previousState = this.networkState; boolean previousExtraRow = hasExtraRow(); this.networkState = newNetworkState; boolean newExtraRow = hasExtraRow(); if (previousExtraRow != newExtraRow) { if (previousExtraRow) { notifyItemRemoved(getItemCount()); } else { notifyItemInserted(getItemCount()); } } else if (newExtraRow && previousState != newNetworkState) { notifyItemChanged(getItemCount() - 1); } }
hasExtraRow()
check the Network State and return true or false. To tell the PagedListAdapter
how to compute the difference between the two elements, you’ll need to implement a new class, DiffCallback.Here, you will define two things.
public class User { public static DiffUtil.ItemCallback<User> DIFF_CALLBACK = new DiffUtil.ItemCallback<User>() { @Override public boolean areItemsTheSame(@NonNull User oldItem, @NonNull User newItem) { return oldItem.userId == newItem.userId; } @Override public boolean areContentsTheSame(@NonNull User oldItem, @NonNull User newItem) { return oldItem.equals(newItem); } }; @Override public boolean equals(Object obj) { if (obj == this) return true; User user = (User) obj; return user.userId == this.userId && user.firstName == this.firstName; } .... }
You will define how to compute whether the contents are the same, and how to define whether the items are the same. Let’s look at the adapter.So our adapter would extend PagedListAdapter
, and then it will connect the user, which is the information that’s being displayed, with the user ViewHolder
.
public class UserAdapter extends PagedListAdapter<User, RecyclerView.ViewHolder> { private static final String TAG = "UserAdapter"; private NetworkState networkState; private ListItemClickListener itemClickListener; public UserAdapter(ListItemClickListener itemClickListener) { super(User.DIFF_CALLBACK); this.itemClickListener = itemClickListener; } @Override public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { LayoutInflater layoutInflater = LayoutInflater.from(parent.getContext()); View view; if (viewType == R.layout.item_user_list) { view = layoutInflater.inflate(R.layout.item_user_list, parent, false); return new UserItemViewHolder(view); } else if (viewType == R.layout.network_state_item) { view = layoutInflater.inflate(R.layout.network_state_item, parent, false); return new NetworkStateItemViewHolder(view, itemClickListener); } else { throw new IllegalArgumentException("unknown view type"); } } @Override public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) { switch (getItemViewType(position)) { case R.layout.item_user_list: ((UserItemViewHolder) holder).bindTo(getItem(position)); break; case R.layout.network_state_item: ((NetworkStateItemViewHolder) holder).bindView(networkState); break; } } private boolean hasExtraRow() { if (networkState != null && networkState != NetworkState.LOADED) { return true; } else { return false; } } @Override public int getItemViewType(int position) { if (hasExtraRow() && position == getItemCount() - 1) { return R.layout.network_state_item; } else { return R.layout.item_user_list; } } public void setNetworkState(NetworkState newNetworkState) { .... } }
We define the callback, the DIFF_CALLBACK, for our user objects and then in onBindViewHolder
, all we need to do is bind the item to the ViewHolder
.
7.Create RecyclerView
In the onCreate, we get the reference to our ViewModel and get the reference to the RecyclerView, and we create our adapter.
RecyclerView recyclerView = findViewById(R.id.userList); LinearLayoutManager llm = new LinearLayoutManager(this); llm.setOrientation(LinearLayoutManager.VERTICAL); recyclerView.setLayoutManager(llm); UserViewModel viewModel = ViewModelProviders.of(this).get(UserViewModel.class); viewModel.init(userDao); final UserAdapter userUserAdapter = new UserAdapter(); viewModel.userList.observe(this, pagedList -> { userUserAdapter.submitList(pagedList); }); viewModel.networkState.observe(this, networkState -> { userUserAdapter.setNetworkState(networkState); Log.d(TAG, "Network State Change"); }); recyclerView.setAdapter(userUserAdapter);
Currently I am using Android Architecture Components for App development everything is working along with paging library Now I want to remove recyclerview Item using PagedListAdapter to populate this we required to add a data source and from data source list is updating using LiveData no I want to remove a item from list notifyItemRemoved() is working from PagedList I am getting this exception:
java.lang.UnsupportedOperationException
java.util.AbstractList.remove(AbstractList.java:638)