Network Paging With Paged Key DataSource and REST API

Network Paging With Paged Key DataSource and REST API

Paging Library is the library provided by Google as part of Architecture Component for using the pagination of the resource. It is helpful to create the pagination from Database using Room as well as with network resource. Here in this article, we will explore the how to use it for Network Paging with backend DataSource.

Why Paging required?

Imagine a scenario where you have thousands of records on your backend which you have to display to the user, but not all data only 10 records. You can’t show all the records to the user at once. If you are going to load all the records from backed and display only 10 or 100 records to the user at once.

So why we should load the all data if we just showing only 10 or 100 records at a time. Here the concept of pagination comes into the picture. There are several ways to achieve that but recently Google has released some of the very useful libraries as part fo Architecture Components.

Paging Library is one of that which help to create the mobile app with pagination. In our last article, I have explained how to do the pagination with Room Database now in this article we will learn how to do paging with Network resource.

If you want to go through the same basic component of the Paging Library you can refer the earlier tutorial on Paging.

Network Paging DataSource

DataSource is one of the very important components of the Paging Library. It is responsible for loading the data snapshots of Page into PagedList.

There are many types of the DataSource which we can use depending on our requirement for Network Paging and scenario of the backend. Below is the link to all type of the DataSource you can referer the same to learn the concept of the all these.

In this article, we will implement the PagedKeyedDataSource to retrieve the page using the REST API from a WordPress enable websites.

As explained above we are going to create an app which will implement the PagedKeyedDataSource to load the pages over the network. We will use the Retrofit, ViewModel, and LiveData for our Network Paging Demo app.

So now, Create a project using the android studio and follow the steps given below to create the app.

Dependency Injection to Project

First of all, Kindly add the below dependency into your project level build.gradle files which is required for this project.

// ViewModel and LiveData
    implementation "android.arch.lifecycle:extensions:1.1.0"
    annotationProcessor "android.arch.lifecycle:compiler:1.1.0"
    // Paging
    implementation "android.arch.paging:runtime:1.0.0-alpha5"
    // for REST API
    implementation 'com.squareup.retrofit2:retrofit:2.3.0'
    implementation 'com.squareup.retrofit2:converter-gson:2.3.0'
    
    implementation 'com.android.support:cardview-v7:26.1.0'
    implementation 'com.android.support:recyclerview-v7:26.1.0'
    //GSON
    implementation 'com.google.code.gson:gson:2.8.1'

Create Model and POJO for Network Paging

Next, we will create our basic model required for our project or we can say simply POJO for our application. In this example, we will just retrieve the heading of all post using the REST.

So create the POJO of the application as given below.

Title.java

package com.nplix.pagedkeydatasourceexample;

import com.google.gson.annotations.Expose;
import com.google.gson.annotations.SerializedName;

public class Title {

    @SerializedName("rendered")
    @Expose
    private String rendered;

    public String getRendered() {
        return rendered;
    }

    public void setRendered(String rendered) {
        this.rendered = rendered;
    }

}

Post.java

package com.nplix.pagedkeydatasourceexample;

import android.support.annotation.NonNull;
import android.support.v7.recyclerview.extensions.DiffCallback;

import com.google.gson.annotations.Expose;
import com.google.gson.annotations.SerializedName;

public class Post {
    @SerializedName("id")
    @Expose
    private Integer id;

    @SerializedName("title")
    @Expose
    private Title title;

    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    public Title getTitle() {
        return title;
    }

    public void setTitle(Title title) {
        this.title = title;
    }


    public Post(final int id, final Title title) {
        this.id = id;
        this.title = title;
    }
//Below DIFF_CALLBACK we will use in our Adapter
    public static DiffCallback<Post> DIFF_CALLBACK = new DiffCallback<Post>() {
        @Override
        public boolean areItemsTheSame(@NonNull Post oldItem, @NonNull Post newItem) {
            return oldItem.id == newItem.id;
        }

        @Override
        public boolean areContentsTheSame(@NonNull Post oldItem, @NonNull Post newItem) {
            return oldItem.equals(newItem);
        }
    };
}

Create WP REST Service Using Retrofit

Next, we will create our REST API using Retrofit to retrieve the post from the network. Let’s create the WP REST Service and API class as given below.

WPRESTService.java

package com.nplix.pagedkeydatasourceexample;

import java.util.List;

import retrofit2.Call;
import retrofit2.http.GET;
import retrofit2.http.Query;


public interface WPRESTService {
    @GET("wp/v2/posts")
    Call<List<Post>> getAllPostByPage(@Query("page") int id);
}

WPRESTAPI.java

package com.nplix.pagedkeydatasourceexample;

import java.util.List;

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


public final class WPRestAPI {
    private final WPRESTService wordPressService;
    static String base_url="https://www.nplix.com/wp-json/";
    private WPRestAPI(){
        Retrofit retrofit=new Retrofit.Builder()
                .baseUrl(base_url)
                .addConverterFactory(GsonConverterFactory.create())
                .build();
        wordPressService=retrofit.create(WPRESTService.class);
    }

    public static WPRestAPI getInstance(){
        return InstanceHolder.INSTASNCE;
    }

    public final Call<List<Post>> getAllPost(int page){
        return wordPressService.getAllPostByPage(page);
    }

    private static final class InstanceHolder {
        private static final WPRestAPI INSTASNCE = new WPRestAPI();
    }
}

Create Helper Class for Network API Response

We will now create the helper class for our app to keep the status of our API CALL. It has three status like running, Sucess and fails.

Status.java

package com.nplix.pagedkeydatasourceexample;

public enum Status {
    RUNNING,
    SUCCESS,
    FAILED,
    MAX
}

NetworkState.java

package com.nplix.pagedkeydatasourceexample;

class NetworkState {
    private final Status status;
    private final String msg;

    static final NetworkState LOADED;
    static final NetworkState LOADING;
    static final NetworkState MAXPAGE;

    NetworkState(Status status, String msg) {
        this.status = status;
        this.msg = msg;
    }

    static {
        LOADED=new NetworkState(Status.SUCCESS,"Success");
        LOADING=new NetworkState(Status.RUNNING,"Running");
        MAXPAGE=new NetworkState(Status.MAX,"No More page"); 
    }

    Status getStatus() {
        return status;
    }

    String getMsg() {
        return msg;
    }

}

Create PagedKeyed DataSource

This is the core class which will responsible for fetching the pages of records from the network. This class as two methods which we will override to full fill the requirement of our app.

First is the loadInitial(): This method is responsible to load the data initially when application launch for the first time, Second is the loadAfter() method it is responsible for the subsequent call to load the data page wise.

So create the DataSource class as given below as given below.

WPDataSource.java


package com.nplix.pagedkeydatasourceexample;
import android.arch.lifecycle.MutableLiveData;
import android.arch.paging.PageKeyedDataSource;
import android.support.annotation.NonNull;
import android.util.Log;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Executor;
import retrofit2.Call;
import retrofit2.Callback;
import retrofit2.Response;
/**
* Created by PK on 2/16/2018.
*/
public class WPDataSource extends PageKeyedDataSource {
private static String TAG="WP DATA SOURCE";
int page=1;
int max_page;
WPRestAPI wordPressService;
LoadInitialParams initialParams;
// ItemKeyedDataSource.LoadInitialParams initialParams;
LoadParams afterParams;
private MutableLiveData networkState;
private MutableLiveData initialLoading;
private Executor retryExecutor;
public WPDataSource(Executor retryExecutor){
wordPressService=WPRestAPI.getInstance();
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 params, @NonNull LoadInitialCallback callback) {
Log.i(TAG, "Loading Page " + 1 + " Load Size " + params.requestedLoadSize);
final List postList =new ArrayList<>();
initialParams=params;
initialLoading.postValue(NetworkState.LOADING);
networkState.postValue(NetworkState.LOADING);
wordPressService.getAllPost(page).enqueue(new Callback<List>() {
@Override
public void onResponse(Call<List> call, Response<List> response) {
if (response.isSuccessful() && response.code() == 200) {
postList.addAll(response.body());
String totalPage=response.headers().get("X-WP-TotalPages");
max_page=Integer.parseInt(totalPage);
Log.d(TAG,"Total page is " + totalPage);
callback.onResult(postList,null,page+1);
initialLoading.postValue(NetworkState.LOADED);
networkState.postValue(NetworkState.LOADED);
initialParams = null;
} else {
Log.e("WP 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> call, Throwable t) {
String errorMessage;
errorMessage = t.getMessage();
if (t == null) {
errorMessage = "error";
}
networkState.postValue(new NetworkState(Status.FAILED, errorMessage));
}
});
}
@Override
public void loadBefore(@NonNull LoadParams params, @NonNull LoadCallback callback) {
}
@Override
public void loadAfter(@NonNull LoadParams params, @NonNull LoadCallback callback) {
Log.i(TAG, "Loading Page " + params.key + " Size " + params.requestedLoadSize);
page=page+1;
final List postList =new ArrayList<>();
afterParams=params;
initialLoading.postValue(NetworkState.LOADING);
networkState.postValue(NetworkState.LOADING);
wordPressService.getAllPost((int) params.key).enqueue(new Callback<List>() {
@Override
public void onResponse(Call<List> call, Response<List> response) {
if (response.isSuccessful() && response.code() == 200) {
postList.addAll(response.body());
// callback.onResult(postList);
callback.onResult(postList, page + 1);
initialLoading.postValue(NetworkState.LOADED);
networkState.postValue(NetworkState.LOADED);
initialParams = null;
}
else if((int)params.key>max_page) {
Log.e("WP API CALL", response.message()+(int)params.key);
initialLoading.postValue(new NetworkState(Status.MAX,response.message()));
networkState.postValue(new NetworkState(Status.MAX, response.message()));
}
else {
Log.e("WP 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> call, Throwable t) {
String errorMessage;
errorMessage = t.getMessage();
if (t == null) {
errorMessage = " error";
}
networkState.postValue(new NetworkState(Status.FAILED, errorMessage));
}
});
}
}

In above class, we are actually calling our rest API from both methods and providing the page as input to WP REST API method and publishing the data using the live data and data source call back using which our Data Source Factory get the updated data, which we are going create below.

DataSource Factory

DataSource Factory is responsible for retrieving the data using the DataSource and Paged list configuration which we will create in later in this article in our ViewModel class.

WPDataSourceFactory.java

package com.nplix.pagedkeydatasourceexample;
import android.arch.lifecycle.MutableLiveData;
import android.arch.paging.DataSource;
import java.util.concurrent.Executor;
/**
* Created by PK on 2/16/2018.
*/
public class WPDataSourceFactory implements DataSource.Factory {
MutableLiveData<WPDataSource> mutableLiveData;
// private WPDataSource wpDataSource;
private WPDataSource wpDataSource;
private Executor executor;
WPDataSourceFactory(Executor executor){
this.executor=executor;
this.mutableLiveData= new MutableLiveData<>();
}
@Override
public DataSource create() {
wpDataSource=new WPDataSource(executor);
mutableLiveData.postValue(wpDataSource);
return wpDataSource;
}
MutableLiveData<WPDataSource> getMutableLiveData() {
return mutableLiveData;
}
}

ViewModel

Now time to create the ViewModel as given below. From the ViewModel, we are setting the PagedList configuration like setInitialLoadPageSizeHint and PageSize for Network Paging Example.

SetInitialLoadPageSize is the hint of the data source to load how many pages as initial and PageSize is the size of the page load in advance.

PostViewModel.java

package com.nplix.pagedkeydatasourceexample;
import android.arch.core.util.Function;
import android.arch.lifecycle.LiveData;
import android.arch.lifecycle.Transformations;
import android.arch.lifecycle.ViewModel;
import android.arch.paging.LivePagedListBuilder;
import android.arch.paging.PagedList;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
/**
* Created by PK on 2/11/2018.
*/
public class PostViewModel extends ViewModel {
LiveData<PagedList<Post>> pagedListLiveData;
LiveData<NetworkState> networkState;
Executor executor;
LiveData<WPDataSource> myDataSource;
public PostViewModel(){
executor= Executors.newFixedThreadPool(5);
WPDataSourceFactory wpDataSourceFactory=new WPDataSourceFactory(executor);
myDataSource=wpDataSourceFactory.getMutableLiveData();
networkState= Transformations.switchMap(myDataSource,
(Function<WPDataSource, LiveData<NetworkState>>) WPDataSource::getNetworkState);
PagedList.Config pagedListConfig =
(new PagedList.Config.Builder()).setEnablePlaceholders(false)
.setInitialLoadSizeHint(2)
.setPageSize(4).build();
pagedListLiveData = (new LivePagedListBuilder(wpDataSourceFactory, pagedListConfig))
.setBackgroundThreadExecutor(executor)
.build();
}
}

Now we are almost to the final stage of this app so we need to create some additional layout for PostItem and MainActivity.

Create Layout

Our layout files are as given below for our example.

activity_main.xml

<?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"
tools:context="com.nplix.pagedkeydatasourceexample.MainActivity">
<android.support.v7.widget.RecyclerView
android:id="@+id/postList"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</android.support.constraint.ConstraintLayout>

post_item.xml

<?xml version="1.0" encoding="utf-8"?>
<android.support.v7.widget.CardView
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="2dp">
<TextView
android:id="@+id/title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textAppearance="@style/TextAppearance.AppCompat.Caption"
android:textIsSelectable="true"
android:textSize="20sp"
android:textStyle="bold"
android:visibility="visible" />
</android.support.v7.widget.CardView>

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"/>
<Button
android:visibility="gone"
android:id="@+id/retry_button"
style="@style/Widget.AppCompat.Button.Colored"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:text="Retry"/>
</LinearLayout>

PagedListAdapter

At last, we have to create an adapter for recycler view by extending the PagedListAdapter. If you want to learn about the PagedListADapter. In Simple, it is a Recycler View Adapter to show the PagedList in the View.

package com.nplix.pagedkeydatasourceexample;
import android.arch.paging.PagedListAdapter;
import android.support.v7.widget.RecyclerView;
import android.text.Html;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.ProgressBar;
import android.widget.TextView;
public class PostAdapter extends PagedListAdapter<Post, RecyclerView.ViewHolder> {
private static final String TAG = "PostAdapter";
private NetworkState networkState;
PostAdapter() {
super(Post.DIFF_CALLBACK);
}
@Override
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
LayoutInflater layoutInflater = LayoutInflater.from(parent.getContext());
View view;
if (viewType == R.layout.post_item) {
view = layoutInflater.inflate(R.layout.post_item, parent, false);
return new PostItemViewHolder(view);
} else if (viewType == R.layout.network_state_item) {
view = layoutInflater.inflate(R.layout.network_state_item, parent, false);
return new NetworkStateItemViewHolder(view);
} else {
throw new IllegalArgumentException("unknown type");
}
}
@Override
public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
switch (getItemViewType(position)) {
case R.layout.post_item:
((PostItemViewHolder) 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 && networkState!=NetworkState.MAXPAGE) {
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.post_item;
}
}
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);
}
}
static class PostItemViewHolder extends RecyclerView.ViewHolder {
TextView title;
PostItemViewHolder(View itemView) {
super(itemView);
title = itemView.findViewById(R.id.title);
}
void bindTo(Post post) {
title.setText(Html.fromHtml(post.getTitle().getRendered()));
}
}
static class NetworkStateItemViewHolder extends RecyclerView.ViewHolder {
private final ProgressBar progressBar;
private final TextView errorMsg;
private Button button;
NetworkStateItemViewHolder(View itemView) {
super(itemView);
progressBar = itemView.findViewById(R.id.progress_bar);
errorMsg = itemView.findViewById(R.id.error_msg);
button = itemView.findViewById(R.id.retry_button);
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
}
});
}
void bindView(NetworkState networkState) {
if (networkState != null && networkState.getStatus() == Status.RUNNING) {
progressBar.setVisibility(View.VISIBLE);
} else {
progressBar.setVisibility(View.GONE);
}
if (networkState != null && networkState.getStatus() == Status.FAILED) {
errorMsg.setVisibility(View.VISIBLE);
errorMsg.setText(networkState.getMsg());
}else if (networkState!=null && networkState.getStatus() ==Status.MAX) {
errorMsg.setVisibility(View.VISIBLE);
errorMsg.setText("No More Page to Load");
}
else
{
errorMsg.setVisibility(View.GONE);
}
}
}
}

We have completed our implementation part just one more modification that we need is connecting altogether from Main Activity class.

Required Permission and Main Activity

So Let’s modify the Main Activity Class and add required permission to AndroidManifest.xml.

We only required INTERNET permission so we need to add the below code in AndroidManifest.xml file.

<uses-permission android:name="android.permission.INTERNET"/>

MainActivity.java

The main part we are doing from our Main Activity is the observing the changes using the ViewModel and its observer and In case of any change in the data of the ViewModel, UI will get updated.

package com.nplix.pagedkeydatasourceexample;
import android.arch.lifecycle.ViewModelProviders;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.util.Log;
public class MainActivity extends AppCompatActivity {
private PostViewModel viewModel;
private String TAG = "Main Activity";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
RecyclerView recyclerView = findViewById(R.id.postList);
LinearLayoutManager llm = new LinearLayoutManager(this);
llm.setOrientation(LinearLayoutManager.VERTICAL);
recyclerView.setLayoutManager(llm);
viewModel = ViewModelProviders.of(this).get(PostViewModel.class);
final PostAdapter postAdapter = new PostAdapter();
viewModel.pagedListLiveData.observe(this, pagedList -> {
postAdapter.setList(pagedList);
});
viewModel.networkState.observe(this, networkState -> {
postAdapter.setNetworkState(networkState);
Log.d(TAG, "Network Changed");
});
recyclerView.setAdapter(postAdapter);
}
}

Now we can run and test our app.

Network Paging Example

Conclusion

If we will go step by step then implementing the Paging Library is a very easy task. However, it may need some concentration and time initially for understanding the logic. The core of the above example is the how data follow from DataSource to UI of the App.

 

Get Email Updates!

Signup now receive an email once I publish new content. I will never give away, trade or sell your email address. You can unsubscribe at any time.

Join 911 other subscribers

Leave a Reply

Close Menu