This post contains affiliate links. If you use these links to buy something I may earn a commission. Thanks.”
We are using lots of apps that load data while we scrolling. Facebook, Instagram, and more apps using this technique. This technique helps users to wait less time.
So if you are planning to make an app like that, using RecyclerView. Then this post is for you.
In this post, You will learn how to use Load More on Scroll or pagination or endless scrolling or infinite scroll or whatever you call, with RecyclerView.
Before that, I recommend reading our RecyclerView Tutorial Guide.
This post is not dealing with the pagination library.
Contents
How To Implement Pagination In Android RecyclerView
In this post, I am going to talk about two methods, that implement pagination in RecyclerView. But in both methods, we will use scroll Listener with RecyclerView to listen to scroll events. When RecyclerView scrolls, onScrolled() method gets called.
So first learn how to add OnScrollListener with Recyclerview.
How To Add OnScrollListener With RecyclerView
//attaches scrollListener with RecyclerView recyclerview.addOnScrollListener(object : RecyclerView.OnScrollListener() { override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) { super.onScrolled(recyclerView, dx, dy) } })
Simple Android RecyclerView Load More On Scroll Example
Let’s make a linear list Using RecyclerView and add an endless scroll feature.
After the implementation of scroll listener, we should do the following
- we use findLastCompletelyVisibleItemPosition() to get the position of last visible item.
- Use a variable which checks progressbar loading or not
- If last item reached and progressbar not loading, loadmore() gets called. Using Handler, we add “load” in list.
- When adapter checks list, ProgressBar gets added.
- Next data chunks received, ProgressBar gets removed and data got added.
- For showing different View types. here
- Data row
- ProgressBar
Make use of getViewType() method in RecyclerView.Adapter.
MainActivity.kt
class MainActivity : AppCompatActivity() { //handler instance var handler: Handler = Handler() //list for holding data lateinit var list : ArrayListlateinit var adapter : RecyclerViewAdapter //Variable for checking progressbar loading or not private var isLoading: Boolean = false lateinit var layoutManager : LinearLayoutManager override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) layoutManager = LinearLayoutManager(this) //attaches LinearLayoutManager with RecyclerView recyclerview.layoutManager = layoutManager list = ArrayList() load() adapter = RecyclerViewAdapter(list) recyclerview.adapter = adapter addScrollerListener() } private fun addScrollerListener() { //attaches scrollListener with RecyclerView recyclerview.addOnScrollListener(object : RecyclerView.OnScrollListener() { override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) { super.onScrolled(recyclerView, dx, dy) if (!isLoading) { //findLastCompletelyVisibleItemPostition() returns position of last fully visible view. ////It checks, fully visible view is the last one. if (layoutManager.findLastCompletelyVisibleItemPosition() == list.size - 1) { loadMore() isLoading = true } } } }) } private fun loadMore() { //notify adapter using Handler.post() or RecyclerView.post() handler.post(Runnable { list.add("load") adapter.notifyItemInserted(list.size - 1) }) handler.postDelayed(Runnable { //removes "load". list.removeAt(list.size - 1) var listSize = list.size adapter.notifyItemRemoved(listSize) //sets next limit var nextLimit = listSize + 10 for(i in listSize until nextLimit) { list.add("Item No $i") } adapter.notifyDataSetChanged() isLoading = false },2500) } private fun load() { for(i in 0..9) { list.add("Item No: $i") } } }
- It's better to use Handler.post() or RecyclerView.post() while updating adapter when RecyclerView measures.
- findLastCompletelyVisibleItemPosition() returns only completely visible Item's position, So make sure your last view is fully visible.
- 2500 milliseconds - delay time
RecyclerViewAdapter.kt
package m.example.simplerecyclerviewloadmoreex_kotlin import androidx.recyclerview.widget.RecyclerView import kotlinx.android.synthetic.main.row.view.* import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.Toast import java.lang.IllegalArgumentException class RecyclerViewAdapter(var list: ArrayList<String>) : RecyclerView.Adapter<RecyclerView.ViewHolder>() { companion object { private const val VIEW_TYPE_DATA = 0; private const val VIEW_TYPE_PROGRESS = 1; } override fun onCreateViewHolder(parent: ViewGroup, viewtype: Int): RecyclerView.ViewHolder { return when (viewtype) { VIEW_TYPE_DATA -> {//inflates row layout val view = LayoutInflater.from(parent.context).inflate(R.layout.row,parent,false) DataViewHolder(view) } VIEW_TYPE_PROGRESS -> {//inflates progressbar layout val view = LayoutInflater.from(parent.context).inflate(R.layout.progressbar,parent,false) ProgressViewHolder(view) } else -> throw IllegalArgumentException("Different View type") } } override fun getItemCount(): Int { return list.size } override fun getItemViewType(position: Int): Int { var viewtype = list.get(position) return when(viewtype) {//if data is load, returns PROGRESSBAR viewtype. "load" -> VIEW_TYPE_PROGRESS else -> VIEW_TYPE_DATA } } override fun onBindViewHolder(holder: RecyclerView.ViewHolder, p1: Int) { if (holder is DataViewHolder) { holder.textview.text = list.get(p1) } } inner class DataViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { var textview = itemView.textview init { itemView.setOnClickListener { Toast.makeText(itemView.context,list.get(adapterPosition),Toast.LENGTH_LONG).show() } } } inner class ProgressViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) }
- getItemViewType(int position) method returns VIEW_TYPE_PROGRESS, if data is "load". Otherwise, it returns VIEW_TYPE_DATA.
- Based on ViewType, In onCreateViewHolder layout will get inflated.
Grid RecyclerView Pagination Example - Different Approach
Now we will make a Grid RecyclerView with pagination. Sometimes we need to load data before the last item loads.
For that purpose, you can make use of this approach.
Okay, what we do in this approach.
we define 3 variables, visibleThreshold, lastVisibleItem, and totalItemCount.
- visibleThreshold : limit of items that user want to see before loading next items
- lastVisibleItem : just like name, last visible item position in adapter
- totalItemCount : No.of items in adapter
after the scroll listener setup,
- When onScrolled() gets called, just checks totalItemCount <= lastVisibleItem + visibleThreshold with isLoading : variable for storing progressbar state.
- if both are true, next chunks of data get loaded
class MainActivity : AppCompatActivity() { lateinit var list : ArrayListlateinit var adapter : RecyclerViewAdapter private var isLoading: Boolean = false lateinit var gridlayoutManager : GridLayoutManager private var visibleThreshold = 5 private var lastVisibleItem = 0 private var totalItemCount = 0 //handler instance private var handler: Handler = Handler() override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) //no of columns or span in the grid. var columns = 2 gridlayoutManager = GridLayoutManager(this, columns) recyclerview.layoutManager = gridlayoutManager load() adapter = RecyclerViewAdapter(list) recyclerview.adapter = adapter addScrollerListener() gridlayoutManager.spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() { //Each item occupies 1 span by default. override fun getSpanSize(p0: Int): Int { return when (adapter.getItemViewType(p0)) { //returns total no of spans, to occupy full sapce for progressbar RecyclerViewAdapter.VIEW_TYPE_PROGRESS -> gridlayoutManager.spanCount RecyclerViewAdapter.VIEW_TYPE_DATA -> 1 else -> -1 } } } } private fun addScrollerListener() { recyclerview.addOnScrollListener(object : RecyclerView.OnScrollListener() { override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) { super.onScrolled(recyclerView, dx, dy) //total no. of items totalItemCount = gridlayoutManager.itemCount //last visible item position lastVisibleItem = gridlayoutManager.findLastCompletelyVisibleItemPosition() if(!isLoading && totalItemCount <= (lastVisibleItem + visibleThreshold)) { loadMore() isLoading = true } } }) } private fun loadMore() { handler.post(Runnable { //adds load item in list list.add("load") adapter.notifyItemInserted(list.size - 1) }) recyclerview.postDelayed(Runnable { //removes load item in list. list.removeAt(list.size - 1) var listSize = list.size adapter.notifyItemRemoved(listSize) //next limit var nextLimit = listSize + 10 for(i in listSize until nextLimit) { list.add("Item No $i") } adapter.notifyDataSetChanged() isLoading = false },7000) } private fun load() { list = ArrayList() for(i in 0..9) { list.add("Item $i") } } }
- Make ProgressBar width to maximum using setSpanSizeLookup() method
RecyclerViewAdapter.kt
class RecyclerViewAdapter(var list: ArrayList<String>) : RecyclerView.Adapter<RecyclerView.ViewHolder>() { companion object { const val VIEW_TYPE_DATA = 0; const val VIEW_TYPE_PROGRESS = 1; } override fun onCreateViewHolder(parent: ViewGroup, viewtype: Int): RecyclerView.ViewHolder { return when (viewtype) { VIEW_TYPE_DATA -> { val view = LayoutInflater.from(parent.context).inflate(R.layout.row,parent,false) DataViewHolder(view) } VIEW_TYPE_PROGRESS -> { val view = LayoutInflater.from(parent.context).inflate(R.layout.progressbar,parent,false) ProgressViewHolder(view) } else -> throw IllegalArgumentException("Different View type") } } override fun getItemCount(): Int { return list.size } override fun getItemViewType(position: Int): Int { var viewtype = list.get(position) return when(viewtype) { "load" -> VIEW_TYPE_PROGRESS else -> VIEW_TYPE_DATA } } override fun onBindViewHolder(p0: RecyclerView.ViewHolder, p1: Int) { if (p0 is DataViewHolder) { p0.textview.text = list.get(p1) } } inner class DataViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { var textview = itemView.textview init { itemView.setOnClickListener { Toast.makeText(itemView.context,list.get(adapterPosition),Toast.LENGTH_LONG).show() } } } inner class ProgressViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) }
Retrofit Android RecyclerView Infinite Scroll Example
In this example, we will load data from the internet using Retrofit2. You can download PHP(data.php) code from the Source Code link shown at the top.
I am using Android Studio 3.5.
Let's create a project.
In choosing your project window, click on "Empty Activity" and click Next.
Name: RetrofitRecyclerViewLoadMoreEx
Package name: androidride.com
Select language Kotlin
I am going to use AndroidX library, so make sure you tick on the androidx.* artifacts.
That's your choice, but I recommend to use this one.
Click "Finish".
implementation 'com.squareup.retrofit2:retrofit:2.0.2' implementation 'com.squareup.retrofit2:converter-gson:2.0.2' implementation 'androidx.cardview:cardview:1.0.0' implementation 'androidx.recyclerview:recyclerview:1.1.0'
- Gson converter : It helps to serialize and deserialize JSON data.
build.gradle(Module:App)
apply plugin: 'com.android.application' apply plugin: 'kotlin-android' apply plugin: 'kotlin-android-extensions' android { compileSdkVersion 28 buildToolsVersion "29.0.2" defaultConfig { applicationId "com.androidride.retrofitrecyclerviewloadmoreex" minSdkVersion 15 targetSdkVersion 28 versionCode 1 versionName "1.0" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } buildTypes { release { minifyEnabled false proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' } } } dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) implementation"org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" implementation 'androidx.appcompat:appcompat:1.1.0' implementation 'androidx.core:core-ktx:1.2.0' implementation 'androidx.constraintlayout:constraintlayout:1.1.3' testImplementation 'junit:junit:4.12' androidTestImplementation 'androidx.test:runner:1.2.0' androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0' implementation 'com.squareup.retrofit2:retrofit:2.0.2' implementation 'com.squareup.retrofit2:converter-gson:2.0.2' implementation 'androidx.cardview:cardview:1.0.0' implementation 'androidx.recyclerview:recyclerview:1.1.0' }
colors.xml
<?xml version="1.0" encoding="utf-8"?> <resources> <color name="colorPrimary">#008577</color> <color name="colorPrimaryDark">#00574B</color> <color name="colorAccent">#D81B60</color> </resources>
strings.xml
<resources> <string name="app_name">Retrofit RecyclerView LoadMore Ex - Kotlin</string> </resources>
styles.xml
<resources> <!-- Base application theme. --> <style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar"> <!-- Customize your theme here. --> <item name="colorPrimary">@color/colorPrimary</item> <item name="colorPrimaryDark">@color/colorPrimaryDark</item> <item name="colorAccent">@color/colorAccent</item> </style> </resources>
- Don't forget to add INTERNET permission in AndroidManifest.xml like below
AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.androidride.retrofitrecyclerviewloadmoreex"> <uses-permission android:name="android.permission.INTERNET"/> <application android:allowBackup="true" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" android:theme="@style/AppTheme"> <activity android:name=".MainActivity"> <intent-filter> <action android:name="android.intent.action.MAIN"/> <category android:name="android.intent.category.LAUNCHER"/> </intent-filter> </activity> </application> </manifest>
Data.kt
package com.androidride.retrofitrecyclerviewloadmoreex data class Data(var category: String) { var title: String? = null var subtitle: String? = null init { this.category = category } }
DataApi.kt
package com.androidride.retrofitrecyclerviewloadmoreex import retrofit2.Call import retrofit2.http.GET import retrofit2.http.Query interface DataApi { @GET("data.php") fun getData(@Query("index") index : Int): Call<List<Data>> }
- In Retrofit2, endpoints defined in Interface with special annotations.
- This endpoint returns List.
MainActivity.kt
package com.androidride.retrofitrecyclerviewloadmoreex import androidx.appcompat.app.AppCompatActivity import android.os.Bundle import android.widget.Toast import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import kotlinx.android.synthetic.main.activity_main.* import retrofit2.Call import retrofit2.Callback import retrofit2.Response import retrofit2.Retrofit class MainActivity : AppCompatActivity() { lateinit var list: ArrayList lateinit var adapter: RecyclerViewAdapter var notLoading = true lateinit var layoutManager: LinearLayoutManager lateinit var api: DataApi lateinit var retrofit: Retrofit override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) list = ArrayList() adapter = RecyclerViewAdapter(list) recyclerview.setHasFixedSize(true) layoutManager = LinearLayoutManager(this) recyclerview.layoutManager = layoutManager recyclerview.adapter = adapter retrofit = RetrofitInstance.getRetrofitInstance() api = retrofit.create(DataApi::class.java) load(0) addscrolllistener() } private fun addscrolllistener() { recyclerview.addOnScrollListener(object : RecyclerView.OnScrollListener() { override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) { if(notLoading && layoutManager.findLastCompletelyVisibleItemPosition() == list.size -1 ) { list.add(Data("progress")) adapter.notifyItemInserted(list.size - 1) notLoading = false val call: Call<List> = api.getData(list.size - 1) call.enqueue( object : Callback<List> { override fun onResponse(call: Call<List>?, response: Response<List>?) { list.removeAt(list.size - 1) adapter.notifyItemRemoved(list.size) if(response!!.body().isNotEmpty()) { list.addAll(response.body()) adapter.notifyDataSetChanged() notLoading = true } else { Toast.makeText(applicationContext,"End of data reached", Toast.LENGTH_LONG).show() } } override fun onFailure(call: Call<List>?, t: Throwable?) { TODO("not implemented") //To change body of created functions use File | Settings | File Templates. } }) } } }) } private fun load(i: Int) { val call: Call<List> = api.getData(0) call.enqueue(object : Callback<List> { override fun onResponse(call: Call<List>?, response: Response<List>?) { if(response!!.isSuccessful) { list.addAll(response!!.body()) adapter.notifyDataSetChanged() } } override fun onFailure(call: Call<List>?, t: Throwable?) { } }) } }
RetrofitInstance.kt
package com.androidride.retrofitrecyclerviewloadmoreex import retrofit2.Retrofit import retrofit2.converter.gson.GsonConverterFactory class RetrofitInstance { companion object { fun getRetrofitInstance(): Retrofit { val retrofit = Retrofit.Builder().baseUrl("https://www.androidride.com/").addConverterFactory( GsonConverterFactory.create()).build() return retrofit } } }
- For performing network requests, we need to create Retrofit Instance using Retrofit.Builder() using Base Url
RecyclerViewAdapter.kt
package com.androidride.retrofitrecyclerviewloadmoreex; import android.content.Context; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.TextView; import android.widget.Toast; import androidx.annotation.NonNull; import androidx.recyclerview.widget.RecyclerView; import org.w3c.dom.Text; import java.util.ArrayList; public class RecyclerViewAdapter extends RecyclerView.Adapter { public static final int TYPE_DATA = 0; public static final int TYPE_PROGRESS = 1; ArrayList list; public RecyclerViewAdapter(Context context,ArrayList list) { this.list = list; } @NonNull @Override public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { LayoutInflater inflater = LayoutInflater.from(parent.getContext()); if(viewType == TYPE_DATA) { return new DataHolder(inflater.inflate(R.layout.row,parent,false)); } else { return new ProgressHolder(inflater.inflate(R.layout.progressbar,parent,false)); } } @Override public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) { if(holder instanceof DataHolder) { ((DataHolder) holder).textTitle.setText(list.get(position).title); ((DataHolder) holder).textSubtitle.setText(list.get(position).subtitle); } } @Override public int getItemCount() { return list == null ? 0 : list.size() ; } @Override public int getItemViewType(int position) { if(list.get(position).category.equals("data")) { return TYPE_DATA; } else { return TYPE_PROGRESS; } } class DataHolder extends RecyclerView.ViewHolder { TextView textTitle,textSubtitle; public DataHolder(@NonNull final View itemView) { super(itemView); textTitle =(TextView)itemView.findViewById(R.id.title); textSubtitle = (TextView)itemView.findViewById(R.id.subtitle); itemView.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { Toast.makeText(itemView.getContext(),"Clicked Item: "+list.get(getAdapterPosition()).subtitle,Toast.LENGTH_SHORT).show(); } }); } } class ProgressHolder extends RecyclerView.ViewHolder { public ProgressHolder(@NonNull View itemView) { super(itemView); } } }
- Efficient Way To Make APK In Less Build Time
- Reduce Android App Development Time Using Genymotion free emulator - for personal use
- Run through WiFi
Thanks for scrolling. If you have liked this post, please share it with your friends and family.