Endless Scrolling with RecyclerView using Retrofit in Android

Endless Scrolling with RecyclerView using Retrofit in Android

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

Project Structure Endless Scrolling
Project Structure Endless Scrolling

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:

Endless Scrolling Output

Source code: Download