fbpx
How to add Load More / Infinite Scrolling in Android using Kotlin

How to add Load More / Infinite Scrolling in Android using Kotlin

In this tutorial, I’m going to show you how to add the Load More / Infinite Scrolling feature in the RecyclerView that has a Linear, Grid or Staggered Grid Layout.

But first, let’s set up the files which are important for whatever layout you choose to implement.

Setting up Load More / Infinite Scrolling

First, let’s start by creating the layout that contains the circular progress bar we show when we load more data (progress_loading.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="50dp"
    android:orientation="vertical">

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:gravity="center_horizontal"
        android:orientation="horizontal">

        <ProgressBar
            android:id="@+id/progressbar"
            android:layout_width="24dp"
            android:layout_height="wrap_content"
            android:layout_gravity="center_horizontal" />
    </LinearLayout>

</LinearLayout>

After that, create the ScrollListener for our RecyclerView (RecyclerViewLoadMoreScroll.kt):

class RecyclerViewLoadMoreScroll : RecyclerView.OnScrollListener {

    private var visibleThreshold = 5
    private lateinit var mOnLoadMoreListener: OnLoadMoreListener
    private var isLoading: Boolean = false
    private var lastVisibleItem: Int = 0
    private var totalItemCount:Int = 0
    private var mLayoutManager: RecyclerView.LayoutManager

    fun setLoaded() {
        isLoading = false
    }

    fun getLoaded(): Boolean {
        return isLoading
    }

    fun setOnLoadMoreListener(mOnLoadMoreListener: OnLoadMoreListener) {
        this.mOnLoadMoreListener = mOnLoadMoreListener
    }

    constructor(layoutManager:LinearLayoutManager) {
        this.mLayoutManager = layoutManager
    }

    constructor(layoutManager:GridLayoutManager) {
        this.mLayoutManager = layoutManager
        visibleThreshold *= layoutManager.spanCount
    }

    constructor(layoutManager:StaggeredGridLayoutManager) {
        this.mLayoutManager = layoutManager
        visibleThreshold *= layoutManager.spanCount
    }


    override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
        super.onScrolled(recyclerView, dx, dy)

        if (dy <= 0) return

        totalItemCount = mLayoutManager.itemCount

        if (mLayoutManager is StaggeredGridLayoutManager) {
            val lastVisibleItemPositions =
                (mLayoutManager as StaggeredGridLayoutManager).findLastVisibleItemPositions(null)
            // get maximum element within the list
            lastVisibleItem = getLastVisibleItem(lastVisibleItemPositions)
        } else if (mLayoutManager is GridLayoutManager) {
            lastVisibleItem = (mLayoutManager as GridLayoutManager).findLastVisibleItemPosition()
        } else if (mLayoutManager is LinearLayoutManager) {
            lastVisibleItem = (mLayoutManager as LinearLayoutManager).findLastVisibleItemPosition()
        }

        if (!isLoading && totalItemCount <= lastVisibleItem + visibleThreshold) {
            mOnLoadMoreListener.onLoadMore()
            isLoading = true
        }

    }

    private fun getLastVisibleItem(lastVisibleItemPositions: IntArray): Int {
        var maxSize = 0
        for (i in lastVisibleItemPositions.indices) {
            if (i == 0) {
                maxSize = lastVisibleItemPositions[i]
            } else if (lastVisibleItemPositions[i] > maxSize) {
                maxSize = lastVisibleItemPositions[i]
            }
        }
        return maxSize
    }
}

In this example, we set the visibleThreshold to 5, which means the circular progress bar will show up when the user sees the 5th item from the end of our downloaded data.

Next, we have to create an interface where we are calling to load more data into our RecyclerView (OnLoadMoreListener):

interface OnLoadMoreListener {
    fun onLoadMore()
}

Also, we need to create a file (if you don’t have one already in your project) with a name Constant.kt and paste the following code:

object Constant {
    const val VIEW_TYPE_ITEM = 0
    const val VIEW_TYPE_LOADING = 1
}

This file contains the IDs for our Item View and Loading View, so the RecyclerView can call the right view in the right moment.

Now we go to our file RecyclerViewActivity.kt where the RecyclerView lives, and we’re going to create an ArrayList called loadMoreItemsCells.

loadMoreItemsCells contains the items we load when we show the circular progress bar (Loading View).

Also, we call the methods setRVLayoutManager() and we set our RecyclerView Layout and setRVScrollListener(), where we add the ScrollListener we created before:

class RecyclerViewActivity : AppCompatActivity() {

    lateinit var loadMoreItemsCells: ArrayList<String?>
    lateinit var adapter: Items_RVAdapter
    lateinit var scrollListener: RecyclerViewLoadMoreScroll
    lateinit var mLayoutManager:RecyclerView.LayoutManager

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_recycler_view)

        //** Get the data for our ArrayList

        //** Set the adapter of the RecyclerView
        adapter = Items_RVAdapter(itemsCells)
        adapter.notifyDataSetChanged()
        items_rv.adapter = adapter

        //** Set the Layout Manager of the RecyclerView
        setRVLayoutManager()
        
        //** Set the scrollListerner of the RecyclerView
        setRVScrollListener()
    }
    
    private fun LoadMoreData() {
        //Add the Loading View
        adapter.addLoadingView()
        //Create the loadMoreItemsCells Arraylist
        loadMoreItemsCells = ArrayList()
        //Get the number of the current Items of the main Arraylist
        val start = adapterLinear.itemCount
        //Load 16 more items
        val end = start + 16
        //Use Handler if the items are loading too fast.
        //If you remove it, the data will load so fast that you can't even see the LoadingView
        Handler().postDelayed({
            for (i in start..end) {
                //Get data and add them to loadMoreItemsCells ArrayList 
                loadMoreItemsCells.add("Item $i")
            }
            //Remove the Loading View
            adapterLinear.removeLoadingView()
            //We adding the data to our main ArrayList
            adapterLinear.addData(loadMoreItemsCells)
            //Change the boolean isLoading to false
            scrollListener.setLoaded()
            //Update the recyclerView in the main thread
            items_linear_rv.post {
                adapterLinear.notifyDataSetChanged()
            }
        }, 3000)

    }

}

But we’re not finished yet!

Now in the Recycler’s View Adapter file we’re going to add some methods that add the data we downloaded during Loading (loadMoreItemsCells) to our main ArrayList (itemsCells), and also add methods that know when to show or hide our Loading View:

class Items_RVAdapter(private var itemsCells: ArrayList<String?>) :
    RecyclerView.Adapter<RecyclerView.ViewHolder>() {

    lateinit var mcontext: Context

    class ItemViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView)

    class LoadingViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView)

    fun addData(dataViews: ArrayList<String?>) {
        this.itemsCells.addAll(dataViews)
        notifyDataSetChanged()
    }

    fun getItemAtPosition(position: Int): String? {
        return itemsCells[position]
    }

    fun addLoadingView() {
        //Add loading item
        Handler().post {
            itemsCells.add(null)
            notifyItemInserted(itemsCells.size - 1)
        }
    }

    fun removeLoadingView() {
        //Remove loading item
        if (itemsCells.size != 0) {
            itemsCells.removeAt(itemsCells.size - 1)
            notifyItemRemoved(itemsCells.size)
        }
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
        mcontext = parent.context
        return if (viewType == Constant.VIEW_TYPE_ITEM) {
            val view = LayoutInflater.from(parent.context).inflate(R.layout.item_row, parent, false)
            ItemViewHolder(view)
        } else {
            val view = LayoutInflater.from(mcontext).inflate(R.layout.progress_loading, parent, false)
            LoadingViewHolder(view)
        }
    }

    override fun getItemCount(): Int {
        return itemsCells.size
    }

    override fun getItemViewType(position: Int): Int {
        return if (itemsCells[position] == null) {
            Constant.VIEW_TYPE_LOADING
        } else {
            Constant.VIEW_TYPE_ITEM
        }
    }

    override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
        if (holder.itemViewType == Constant.VIEW_TYPE_ITEM) {
            holder.itemView.itemtextview.text = itemsCells[position]
        }
    }
}

Linear Layout

For a RecyclerView with Linear Layout we have to add the following methods in our RecyclerViewActivity.kt file:

private fun setRVLayoutManager() {
    mLayoutManager = LinearLayoutManager(this)
    items_rv.layoutManager = mLayoutManager
    items_rv.setHasFixedSize(true)
}

private  fun setRVScrollListener() {
    mLayoutManager = LinearLayoutManager(this)
    scrollListener = RecyclerViewLoadMoreScroll(mLayoutManager as LinearLayoutManager)
    scrollListener.setOnLoadMoreListener(object : OnLoadMoreListener {
        override fun onLoadMore() {
            LoadMoreData()
        }
    })
    items_rv.addOnScrollListener(scrollListener)
}

That’s it! Run the project and…

Grid Layout

A RecyclerView with Grid Layout is a little bit different than the other layouts, and we have to add the following methods into our RecyclerViewActivity.kt file:

private fun setRVLayoutManager() {
    mLayoutManager = GridLayoutManager(this, 3)
    items_grid_rv.layoutManager = mLayoutManager
    items_grid_rv.setHasFixedSize(true)
    items_grid_rv.adapter = adapterGrid
    (mLayoutManager as GridLayoutManager).spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() {
        override fun getSpanSize(position: Int): Int {
            return when (adapterGrid.getItemViewType(position)) {
                VIEW_TYPE_ITEM -> 1
                VIEW_TYPE_LOADING -> 3 //number of columns of the grid
                else -> -1
            }
        }
    }
}

private fun setRVScrollListener() {
    scrollListener = RecyclerViewLoadMoreScroll(mLayoutManager as GridLayoutManager)
    scrollListener.setOnLoadMoreListener(object :
        OnLoadMoreListener {
        override fun onLoadMore() {
            LoadMoreData()
        }
    })

    items_grid_rv.addOnScrollListener(scrollListener)
}

The difference in Grid Layout from the others is that we have to set the grid columns to 1 to be the full width of the recyclerView, when we want to show the Loading View.

Let’s run the project!

Staggered Grid Layout

The Layout Manager of a RecyclerView with a Staggered Layout is the same as Linear:

private fun setRVLayoutManager() {
    mLayoutManager = StaggeredGridLayoutManager(2, StaggeredGridLayoutManager.VERTICAL)
    items_rv.layoutManager = mLayoutManager
    items_rv.setHasFixedSize(true)
    items_rv.adapter = adapterStaggered
}

private fun setRVScrollListener() {
    scrollListener = RecyclerViewLoadMoreScroll(mLayoutManager as StaggeredGridLayoutManager)
    scrollListener.setOnLoadMoreListener(object :
        OnLoadMoreListener {
        override fun onLoadMore() {
            LoadMoreData()
        }
    })

    items_staggered_rv.addOnScrollListener(scrollListener)
}

But it’s a little bit tricky when it comes to showing our Loading View in full width.

It doesn’t have the helper class .spanSizeLookup as the Grid Layout has. So we need to add the following code to our RecyclerView’s Adapter:

override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
        if (holder.itemViewType == Constant.VIEW_TYPE_ITEM) {
            holder.itemView.itemtextview.text = itemsCells[position]
        }else{
            val layoutParams = holder.itemView.layoutParams as StaggeredGridLayoutManager.LayoutParams
            layoutParams.isFullSpan = true
        }
    }

And run it!

You can find the final project here

If you have any questions feel free to DM me on Twitter @johncodeos or leave a comment below!

Subscribe
Notify of
guest
9 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
Ivan

Thanks so much for the lesson. However, I would like to know how you can use this method for LinearLayoutManager and an array of type ArrayList<Model> where it is not ArrayList<String>

osama altawil

When pulling up, the recycler view will re-load the data every time the pull is made up, in addition to a value

total_item_count=0

and
lastVisibleItem = -1 and visibleThreshold=5

lastVisibleItem + visibleThreshold=4

Osama AL Tawil

I know that, but I don’t know why it equals -1, I put it on logcat and it equals -1

Osama AL Tawil

yes,I printed last visible item, which is equal to -1, even though item count in adapter = 19 item

osama altawil