This tutorial will teach you how to implement Endless Scrolling in Android applications using RecyclerViews. An infinite scroll displays the loading icon while new records are pulled from the database. Many applications, such as Facebook and Twitter, use this type of loading.
Android RecyclerView Load More
For the Loading icon to appear at the bottom of RecyclerView, we need to use multiple view types in our Android Application.
The RecyclerView needs to implement OnScrollListener() to detect when the user has scrolled to the end.
We will demonstrate Endless Scrolling with RecyclerView by populating a List of Items and loading the next set of Items.
Project Structure
Code
Let’s take a look at the activity_main.xml layout:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context=".MainActivity">
<include
layout="@layout/toolbar"/>
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rv_transaction"
android:layout_above="@id/bottom_progress_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
<include
layout="@layout/bottom_progress_bar"
android:id="@+id/bottom_progress_layout"
android:layout_height="wrap_content"
android:layout_width="wrap_content"
android:layout_centerHorizontal="true"
android:layout_alignParentBottom="true"
android:visibility="gone"/>
<include
layout="@layout/center_progress_bar"
android:layout_height="wrap_content"
android:layout_width="wrap_content"
android:layout_centerInParent="true"
android:visibility="gone"/>
<include
layout="@layout/no_product_dialog"
android:layout_height="wrap_content"
android:layout_width="wrap_content"
android:layout_centerInParent="true"
android:visibility="gone"/>
</RelativeLayout>
</LinearLayout>
The layout_transaction.xml layout:
<?xml version="1.0" encoding="utf-8"?>
<androidx.cardview.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="@dimen/margin_8">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:weightSum="2"
android:padding="@dimen/padding_12">
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:orientation="vertical"
android:layout_marginEnd="@dimen/margin_4">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Transaction Number"
android:textColor="@color/black"
android:textSize="15sp"
android:textStyle="bold"/>
<TextView
android:id="@+id/tv_transaction_number"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Transaction Number"
android:textColor="@color/black"
android:textSize="15sp"
android:layout_marginTop="@dimen/margin_4"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Client Name"
android:textColor="@color/black"
android:textSize="15sp"
android:layout_marginTop="@dimen/margin_8"
android:textStyle="bold"/>
<TextView
android:id="@+id/tv_client_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Client Name"
android:textColor="@color/black"
android:textSize="15sp"
android:layout_marginTop="@dimen/margin_4"/>
</LinearLayout>
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:orientation="vertical"
android:layout_marginStart="@dimen/margin_4">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Date"
android:textColor="@color/black"
android:textSize="15sp"
android:textStyle="bold"/>
<TextView
android:id="@+id/tv_date"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Date"
android:textColor="@color/black"
android:textSize="15sp"
android:layout_marginTop="@dimen/margin_4"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Amount"
android:textColor="@color/black"
android:textSize="15sp"
android:textStyle="bold"
android:layout_marginTop="@dimen/margin_8"/>
<TextView
android:id="@+id/tv_amount"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Amount"
android:textColor="@color/black"
android:textSize="15sp"
android:layout_marginTop="@dimen/margin_4"/>
</LinearLayout>
</LinearLayout>
</androidx.cardview.widget.CardView>
Let’s take a look at the center_progress_bar.xml :
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingTop="@dimen/padding_16"
android:paddingBottom="@dimen/padding_16"
android:paddingStart="@dimen/padding_32"
android:paddingEnd="@dimen/padding_32"
android:orientation="vertical"
android:id="@+id/center_progress_layout">
<ProgressBar
android:id="@+id/progress_bar"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_gravity="center" />
<TextView
android:id="@+id/progress_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/margin_8"
android:gravity="center_vertical"
android:text="Loading ..."
android:textColor="@color/GBL1_5"
android:textSize="15sp"
android:layout_gravity="center"/>
</LinearLayout>
The bottom_progress_bar.xml layout:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingTop="@dimen/padding_16"
android:paddingBottom="@dimen/padding_16"
android:paddingStart="@dimen/padding_32"
android:paddingEnd="@dimen/padding_32"
android:orientation="vertical"
android:id="@+id/bottom_progress_layout">
<ProgressBar
android:id="@+id/progress_bar"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_gravity="center" />
<TextView
android:id="@+id/progress_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/margin_8"
android:gravity="center_vertical"
android:text="Loading ..."
android:textColor="@color/GBL1_5"
android:textSize="15sp"
android:layout_gravity="center"/>
</LinearLayout>
Let’s take a look at the MainActivity.java class where we create and instantiate the Adapter.
package com.infovistar.recylerviewexample;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.widget.Toolbar;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.LinearLayout;
import android.widget.TextView;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import retrofit2.Call;
import retrofit2.Callback;
import retrofit2.Response;
public class MainActivity extends AppCompatActivity {
private Toolbar toolbar;
private TextView toolbarTitle;
private RecyclerView rvTransaction;
private LinearLayout centerProgressLayout;
private LinearLayout bottomProgressLayout;
private LinearLayout noItemLayout;
private LinearLayoutManager layoutManager;
private ApiService apiService;
private int startId = 0;
private int indexId = 0;
private int loadLimit = 50;
private List<TransactionResultModel> resultModelList;
private TransactionAdapter transactionAdapter;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
init();
loadTransactionList(startId);
rvTransaction.addOnScrollListener(new RecyclerView.OnScrollListener() {
@Override
public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) {
super.onScrolled(recyclerView, dx, dy);
if (dy > 0) {
if (!recyclerView.canScrollVertically(RecyclerView.FOCUS_DOWN)) {
layoutManager.scrollToPosition(resultModelList.size() - 1);
indexId++;
startId = loadLimit * indexId;
loadTransactionList(startId);
}
}
}
});
}
private void loadTransactionList(int startId) {
if(startId == 0) {
centerProgressLayout.setVisibility(View.VISIBLE);
rvTransaction.setVisibility(View.GONE);
noItemLayout.setVisibility(View.GONE);
bottomProgressLayout.setVisibility(View.GONE);
} else {
bottomProgressLayout.setVisibility(View.VISIBLE);
}
HashMap<String, Object> hashMap = new HashMap<>();
hashMap.put("start_limit", startId);
hashMap.put("load_limit", loadLimit);
apiService
.getTransactionList(hashMap)
.enqueue(new Callback<TransactionModel>() {
@Override
public void onResponse(Call<TransactionModel> call, Response<TransactionModel> response) {
if(response.body() != null) {
if(response.body().getStatus()) {
if(startId == 0) {
centerProgressLayout.setVisibility(View.GONE);
rvTransaction.setVisibility(View.VISIBLE);
noItemLayout.setVisibility(View.GONE);
}
bottomProgressLayout.setVisibility(View.GONE);
populateTransactionList(response.body().getResult());
} else {
if(startId == 0) {
centerProgressLayout.setVisibility(View.GONE);
centerProgressLayout.setVisibility(View.GONE);
noItemLayout.setVisibility(View.VISIBLE);
}
bottomProgressLayout.setVisibility(View.GONE);
}
}
}
@Override
public void onFailure(Call<TransactionModel> call, Throwable t) {
if(startId == 0) {
centerProgressLayout.setVisibility(View.GONE);
centerProgressLayout.setVisibility(View.GONE);
noItemLayout.setVisibility(View.VISIBLE);
}
bottomProgressLayout.setVisibility(View.GONE);
}
});
}
private void populateTransactionList(List<TransactionResultModel> result) {
resultModelList.addAll(result);
transactionAdapter.notifyDataSetChanged();
}
private void init() {
toolbar = findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
toolbarTitle = findViewById(R.id.toolbar_title);
toolbarTitle.setText(getString(R.string.app_name));
rvTransaction = findViewById(R.id.rv_transaction);
centerProgressLayout = findViewById(R.id.center_progress_layout);
noItemLayout = findViewById(R.id.no_item_layout);
bottomProgressLayout = findViewById(R.id.bottom_progress_layout);
layoutManager = new LinearLayoutManager(this);
rvTransaction.setLayoutManager(layoutManager);
rvTransaction.setHasFixedSize(true);
resultModelList = new ArrayList<>();
transactionAdapter = new TransactionAdapter(resultModelList);
rvTransaction.setAdapter(transactionAdapter);
apiService = ApiRequestHelper.getApiClient(this).create(ApiService.class);
}
class TransactionAdapter extends RecyclerView.Adapter<TransactionAdapter.ViewHolder> {
private List<TransactionResultModel> resultModelList;
public TransactionAdapter(List<TransactionResultModel> resultModelList) {
this.resultModelList = resultModelList;
}
@NonNull
@Override
public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.layout_transaction, parent, false);
return new ViewHolder(view);
}
@Override
public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
TransactionResultModel resultModel = resultModelList.get(position);
holder.setTransactionNumber(resultModel.getTransactionNumber());
holder.setVendorName(resultModel.getVendorName());
holder.setReceiveDate(resultModel.getReceiveDate());
holder.setCurrencyAmount(resultModel.getCurrencyAmount() + " " + resultModel.getCurrency());
}
@Override
public int getItemCount() {
return resultModelList.size();
}
public class ViewHolder extends RecyclerView.ViewHolder {
private TextView tvTransactionNumber;
private TextView tvClientName;
private TextView tvDate;
private TextView tvAmount;
public ViewHolder(@NonNull View itemView) {
super(itemView);
tvTransactionNumber = itemView.findViewById(R.id.tv_transaction_number);
tvClientName = itemView.findViewById(R.id.tv_client_name);
tvDate = itemView.findViewById(R.id.tv_date);
tvAmount = itemView.findViewById(R.id.tv_amount);
}
public void setTransactionNumber(String transactionNumber) {
tvTransactionNumber.setText(transactionNumber);
}
public void setVendorName(String vendorName) {
tvClientName.setText(vendorName);
}
public void setReceiveDate(String receiveDate) {
tvDate.setText(receiveDate);
}
public void setCurrencyAmount(String amount) {
tvAmount.setText(amount);
}
}
}
}
The addOnScrollListener() is the most important method in the above code.
The next list is populated if the bottom-most item is visible in RecyclerView.
The onBindViewHolder() is called to display the data at the specified position. It is used to update the contents of the RecyclerView to reflect the item at the given position.
Retrofit
Retrofit is a Java and Android library for type-safe HTTP networking. The retrofit is super fast, has better functionality, and has a simpler syntax.
Advantages of Retrofit
- Manages the process of receiving, sending, and creating HTTP requests and responses.
- Alternates IP addresses if there is a connection to a web service failure.
- Caches responses to avoid sending duplicate requests.
- Pools connections to reduce latency.
Classes used in retrofit
- Model class – This class contains the objects from the JSON file. For example, TransactionModel.java and TransactionResultModel.java
- Retrofit instance – Used to send requests to an API. For example, ApiRequestHelper.java
- Interface class – Used to define endpoints. For example, ApiService.java
The ApiRequestHelper.java file
package com.infovistar.recylerviewexample;
import android.content.Context;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.security.KeyManagementException;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.cert.Certificate;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.util.concurrent.TimeUnit;
import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManagerFactory;
import javax.net.ssl.X509TrustManager;
import okhttp3.Cache;
import okhttp3.Interceptor;
import okhttp3.OkHttpClient;
import okhttp3.Response;
import retrofit2.Retrofit;
import retrofit2.converter.gson.GsonConverterFactory;
public class ApiRequestHelper {
public static final String WEB_URL = "https://infovistar.com/";
public static final String BASE_URL = WEB_URL+"api/v1/";
public static Retrofit retrofit = null;
public static Context context;
private static final Interceptor REWRITE_CACHE_CONTROL_INTERCEPTOR = new Interceptor() {
@Override
public Response intercept(Chain chain) throws IOException {
Response originalResponse = chain.proceed(chain.request());
if (NetworkDetector.isNetworkAvailable(context)) {
int maxAge = 60; // read from cache for 1 minute
return originalResponse.newBuilder()
.header("Cache-Control", "public, max-age=" + maxAge)
.build();
} else {
int maxStale = 60 * 60 * 24 * 28; // tolerate 4-weeks stale
return originalResponse.newBuilder()
.header("Cache-Control", "public, only-if-cached, max-stale=" + maxStale)
.build();
}
}
};
//setup cache
public static File httpCacheDirectory = new File(context != null ? context.getCacheDir() : null, "responses");
public static int cacheSize = 10 * 1024 * 1024; // 10 MiB
public static Cache cache = new Cache(httpCacheDirectory, cacheSize);
public static final OkHttpClient clientOkHttp = new OkHttpClient().newBuilder()
.readTimeout(120, TimeUnit.SECONDS)
.connectTimeout(120, TimeUnit.SECONDS)
.addInterceptor(REWRITE_CACHE_CONTROL_INTERCEPTOR)
.cache(cache)
.build();
public static Retrofit getApiClient() {
if(retrofit == null) {
retrofit = new Retrofit.Builder().baseUrl(BASE_URL).addConverterFactory(GsonConverterFactory.create()).client(clientOkHttp).build();
}
return retrofit;
}
public static Retrofit getApiClient(Context ctx) {
if(retrofit == null) {
OkHttpClient client;
try {
CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509");
InputStream inputStream = ctx.getResources().openRawResource(R.raw.infovistarcom);
Certificate certificate = certificateFactory.generateCertificate(inputStream);
String keyStoreType = KeyStore.getDefaultType();
KeyStore keyStore = KeyStore.getInstance(keyStoreType);
keyStore.load(null, null);
keyStore.setCertificateEntry("ca", certificate);
String defAlgo = TrustManagerFactory.getDefaultAlgorithm();
TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(defAlgo);
trustManagerFactory.init(keyStore);
SSLContext sslContext = SSLContext.getInstance("TLS");
sslContext.init(null, trustManagerFactory.getTrustManagers(), null);
client = new OkHttpClient()
.newBuilder()
.sslSocketFactory(sslContext.getSocketFactory(), (X509TrustManager) trustManagerFactory.getTrustManagers()[0])
.readTimeout(120, TimeUnit.SECONDS)
.connectTimeout(120, TimeUnit.SECONDS)
.addInterceptor(REWRITE_CACHE_CONTROL_INTERCEPTOR)
.cache(cache)
.build();
retrofit = new Retrofit.Builder().baseUrl(BASE_URL).addConverterFactory(GsonConverterFactory.create()).client(client).build();
} catch (CertificateException | KeyStoreException | IOException | NoSuchAlgorithmException | KeyManagementException e) {
e.printStackTrace();
retrofit = new Retrofit.Builder().baseUrl(BASE_URL).addConverterFactory(GsonConverterFactory.create()).client(clientOkHttp).build();
}
context = ctx;
}
return retrofit;
}
}
The ApiService.java file
package com.infovistar.recylerviewexample;
import java.util.HashMap;
import retrofit2.Call;
import retrofit2.http.FieldMap;
import retrofit2.http.FormUrlEncoded;
import retrofit2.http.POST;
public interface ApiService {
@FormUrlEncoded
@POST("transaction/list")
Call<TransactionModel> getTransactionList(@FieldMap HashMap<String, Object> hashMap);
}
Output:
Source code: Download