Android RecyclerView Mastery: Your Ultimate Guide

Android RecyclerView Mastery: Your Ultimate Guide

Your Complete RecyclerView Guide: How to Handle Clicks, Searches, and Expandable Items.

Introduction

RecyclerView is a flexible way to display a large collection of views. It is more efficient than GirdView, as it recycles the views that are out of visibility and reduces memory consumption.

class RecyclerView : ViewGroup, ScrollingView, NestedScrollingChild2, NestedScrollingChild3

Creating RecyclerView in Android requires :

  1. Item Layout: an XML layout that will be treated as an item for the list created by the RecyclerView.

    Here, we decide how our element in recycler view looks.

  2. ViewHolder: a ViewHolder class that stores the reference to the card layout views that have to be dynamically modified during the program's execution by a list of data objects.

    Here, we extend the ViewHolder class that decides how each element in the list looks and behaves. Our ViewHolder works as a wrapper around our View Object, however, our View Object is sorely managed by the Recycler View.

  3. Data: Here, we use an Adapter class that binds our data with the ViewHolder views.

Key Classes used in Creating RecyclerView in Android :

  1. RecyclerView which is a ViewGroup Class.

  2. A Class extending the RecyclerView.ViewHolder class, as our ViewHolder object, defines how each element looks.

  3. A Class extending the RecyclerView.Adapter class, that binds the view with their data.

  4. LayoutManager abstract class, that arranges the view items in our recycler view.

Implementation

activity_main.xml

<?xml version = "1.0" encoding =   "utf-8" ?>
<androidx.constraintlayout.widget.ConstraintLayout..>
    <ImageView../>
    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/recyclerView"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        tools:listitem="@layout/recyclerview_item_layout"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/imageView" />
</androidx.constraintlayout.widget.ConstraintLayout>

Here, we define our Recycler View Element & link it with our Card View Layout using the tools:listitem attribute.
The Card View Layout : recyclerview_item_layout.xml file defines the Layout for each List Element.

recyclerview_item_layout.xml

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout ..>
    <TextView
        android:id="@+id/headingText"
        android:layout_marginVertical="8dp"
        android:layout_marginHorizontal="15dp"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Heading"
        android:textSize="18sp"
        android:textStyle="bold|italic"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />
    <TextView
        android:id="@+id/descText"
        android:layout_width="match_parent"
        android:layout_height="25dp"
        android:layout_marginHorizontal="15dp"
        android:text="Description"
        android:textSize="15sp"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/headingText" />
</androidx.constraintlayout.widget.ConstraintLayout>

The Height of the Root Element, in our Card View Layout, should preferably be wrap_content or a definite size. As it is mapped multiple times in out recycler view’s list.

Create a definite data class dataRecycler.kt that will make the process of clubbing data of each card view easier.

package com.example.relativelayout
data class dataRecycler(var headingString : String, var descString : String) { }

MainActivity.kt

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        //statements..
        val dataList : ArrayList<dataRecycler> = Constant.getData()
//Making a seperate object to make creating data list easier 
        val rvAdapter  = recyclerAdapter(dataList, this)
        binding.recyclerView.layoutManager = LinearLayoutManager(this)
        binding.recyclerView.adapter = rvAdapter
    }
}
object Constant {
    private lateinit var dataList : ArrayList<dataRecycler>
    fun getData() : ArrayList<dataRecycler>{
        dataList = ArrayList()
        dataList.add(dataRecycler("india", "delhi"))
        dataList.add(dataRecycler("india", "best country"))
        //add data
        return dataList
    }
}

Creating a Kotlin class file for our Adapter & ViewHolder recyclerAdapter.kt

class recyclerAdapter(var dataList : ArrayList<dataRecycler>,var context: Context) 
    : RecyclerView.Adapter<recyclerAdapter.myViewHolder>(){

    inner class myViewHolder(var binding : RecyclerviewItemLayoutBinding) 
    : RecyclerView.ViewHolder(binding.root)

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): myViewHolder {
        val binding = RecyclerviewItemLayoutBinding.inflate(LayoutInflater.from(context), parent, false)
        return myViewHolder(binding)
    }

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

    override fun onBindViewHolder(holder: myViewHolder, position: Int) {
        holder.binding.headingText.text = dataList.get(position).headingString
        holder.binding.descText.text = dataList.get(position).descString

        holder.itemView.setOnClickListener {
            Toast.makeText(context, "hey", Toast.LENGTH_SHORT).show()
        }
    }
}

Implementing Click Methods on Views inside Recycler View

  1. Creating a Listener Interface: Define an interface in the adapter to handle item click events.
class recyclerAdapter(var dataList : ArrayList<dataRecycler>,var context: Context, val listener: OnItemClickListener) : RecyclerView.Adapter<recyclerAdapter.myViewHolder>(){
    inner class myViewHolder(var binding : RecyclerviewItemLayoutBinding) : RecyclerView.ViewHolder(binding.root)

    interface OnItemClickListener {
        fun onItemClick(position: Int) // Click on entire item
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): myViewHolder //statements
    override fun getItemCount(): Int //statements
    override fun onBindViewHolder(holder: myViewHolder, position: Int) //statements
}
  1. Pass the Listener to the Adapter: pass the listener instance from the activity/fragment to the adapter.
class MainActivity : AppCompatActivity(), recyclerAdapter.OnItemClickListener {
    private lateinit var dataList : ArrayList<dataRecycler>
    override fun onCreate(savedInstanceState: Bundle?) {
        //statements
        val rvAdapter  = recyclerAdapter(dataList, this, this)
        //statements
    }
    override fun onItemClick(position: Int) {
        Toast.makeText(this, "Item clicked: ${dataList[position].headingString} + ${dataList[position].descString}", Toast.LENGTH_SHORT).show()
    }
}
  1. Set the Click Listener in onBindViewHolder: Assign the onClick action to the relevant views in the RecyclerView item layout.
class recyclerAdapter(var dataList : ArrayList<dataRecycler>,var context: Context, val listener: OnItemClickListener) : RecyclerView.Adapter<recyclerAdapter.myViewHolder>(){
    inner class myViewHolder(var binding : RecyclerviewItemLayoutBinding) : RecyclerView.ViewHolder(binding.root)
    interface OnItemClickListener{} //statements
    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): myViewHolder { } 
    override fun getItemCount(): Int { } 
    override fun onBindViewHolder(holder: myViewHolder, position: Int) {
        holder.binding.headingText.text = dataList.get(position).headingString
        holder.binding.descText.text = dataList.get(position).descString
        holder.itemView.setOnClickListener {
            listener.onItemClick(position)
        }
    }
}
  1. Handle the Click in the Activity/Fragment: Implement the interface in your activity/fragment and define the logic for handling the click.

This approach of handling the click through the main activity is better than handling it directly from onBind as :

  • The adapter is responsible only for binding data. Click handling is delegated to the listener implemented by the activity or fragment.

  • Reuse of the same adapter across multiple activities/fragments with different click behaviours by simply implementing the OnItemClickListener interface differently.

Horizontal Recycler View

LinearLayoutManager is a class used to define the layout of items in a RecyclerView

  • Context (this): Used to create the layout manager.

  • Orientation (HORIZONTAL or VERTICAL): Determines the direction of the layout.

  • ReverseLayout (true or false): If true, reverses the item order.

val layoutManager = LinearLayoutManager(this, LinearLayoutManager.HORIZONTAL, false)
recyclerView.layoutManager = layoutManager

Other Similar Classes

GridLayoutManager : Arranges items in a grid-like structure with multiple columns or rows

rV.layoutManager = GridLayoutManager(this, 2) //2 columns
//(context, spanCount)

StaggeredGridLayoutManager : Similar to GridLayoutManager, but each item can have a different size (height or width), creating a staggered appearance.

rV.layoutManager = StaggeredGridLayoutManager(2, StaggeredGridLayoutManager.VERTICAL)
//(spanCount, orientation)

Implementing Search in Recycler View

  1. Creating the SearchView user interface above the recycler view.
<androidx.constraintlayout.widget.ConstraintLayout..>
    <ImageView../>
    <SearchView
        android:id="@+id/searchview"
        android:layout_width="match_parent"
        android:layout_height="50dp"
        android:background="@color/white"
        android:queryHint="Search Country"
        android:iconifiedByDefault="false"
        app:queryBackground="@android:color/transparent"
        android:layout_marginHorizontal="10dp"
        app:layout_constraintBottom_toBottomOf="@+id/imageView"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/imageView"
        app:layout_constraintVertical_bias="0.54" />
    <androidx.recyclerview.widget.RecyclerView../>
</androidx.constraintlayout.widget.ConstraintLayout>
  1. Implementing the SearchView functionality through code
class MainActivity : AppCompatActivity(), recyclerAdapter.OnItemClickListener {
    private lateinit var dataList : ArrayList<dataRecycler>
    private lateinit var rvAdapter : recyclerAdapter
    private lateinit var binding: ActivityMainBinding
    override fun onCreate(savedInstanceState: Bundle?) {
        //Code.. 
        dataList= Constant.getData()
        rvAdapter  = recyclerAdapter(dataList, this, this)
        binding.recyclerView.layoutManager = LinearLayoutManager(this)
        binding.recyclerView.adapter = rvAdapter
        binding.searchview.setOnQueryTextListener(object : SearchView.OnQueryTextListener{
            override fun onQueryTextSubmit(p0: String?): Boolean = false
            override fun onQueryTextChange(p0: String?): Boolean {
                filterList(p0)
                return true
            }
        })
    }
    private fun filterList(query : String?){
        if(query != null){
            val filteredList = ArrayList<dataRecycler>()
            for(i in Constant.getData()){
                if(i.headingString.lowercase(Locale.ROOT).contains(query.lowercase(Locale.ROOT)))    filteredList.add(i)
            }
            dataList.clear()
            dataList.addAll(filteredList)
            rvAdapter.notifyDataSetChanged()
        }
    }
    override fun onItemClick(position: Int) {
        Toast.makeText(this, "Item clicked: ${dataList[position].headingString} + ${dataList[position].descString}", Toast.LENGTH_SHORT).show()
    }
}

Expandable Items in Recycler View

  1. Modifying the dataRecycler.kt and recyclerview_item_layout.xml to allow one expandable element and a boolean value to store its state
data class dataRecycler(var headingString : String, var descString : String, 
        var statements : String = "dgzdzgvdxgdgsdgeibgkxbgxdiesgbvdklbgsobgosebigsg", 
        var isExpanded : Boolean = false) { }
<androidx.constraintlayout.widget.ConstraintLayout ..>
    <TextView
        android:id="@+id/headingText"../>
    <TextView
        android:id="@+id/descText"../>
    <TextView
        android:id="@+id/statementText"../>
</androidx.constraintlayout.widget.ConstraintLayout>
  1. Handling Expanding Item when it is clicked
class recyclerAdapter(var dataList : ArrayList<dataRecycler>,var context: Context) : RecyclerView.Adapter<recyclerAdapter.myViewHolder>(){
    inner class myViewHolder(var binding : RecyclerviewItemLayoutBinding) : RecyclerView.ViewHolder(binding.root)
    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): myViewHolder { }
    override fun getItemCount(): Int { }
    override fun onBindViewHolder(holder: myViewHolder, position: Int) {
        val itemBind = dataList[position]
        holder.binding.headingText.text = dataList.get(position).headingString
        holder.binding.descText.text = dataList.get(position).descString
        holder.binding.statementText.text = itemBind.statements
        val isExpanded = itemBind.isExpanded
        holder.binding.statementText.visibility = if(isExpanded) View.VISIBLE else View.GONE

        holder.itemView.setOnClickListener {
            itemBind.isExpanded = !itemBind.isExpanded
            notifyItemChanged(position)
        }
    }
}

Making only a single item view be expanded a time

class recyclerAdapter(var dataList : ArrayList<dataRecycler>,var context: Context) 
                        : RecyclerView.Adapter<recyclerAdapter.myViewHolder>(){
    inner class myViewHolder(var binding : RecyclerviewItemLayoutBinding) 
                                : RecyclerView.ViewHolder(binding.root){
        fun collapseExpandedView(){
            binding.statementText.visibility = View.GONE
        }
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): myViewHolder { }
    override fun getItemCount(): Int { }
    override fun onBindViewHolder(holder: myViewHolder, position: Int) {
        val itemBind = dataList[position]
        holder.binding.headingText.text = dataList.get(position).headingString
        holder.binding.descText.text = dataList.get(position).descString
        holder.binding.statementText.text = itemBind.statements
        val isExpanded = itemBind.isExpanded
        holder.binding.statementText.visibility = if(isExpanded) View.VISIBLE else View.GONE

        holder.itemView.setOnClickListener {
            isAnyExpanded(position)
            itemBind.isExpanded = !itemBind.isExpanded
            notifyItemChanged(position)
        }
    }

    private fun isAnyExpanded(position: Int){
        val temp = dataList.indexOfFirst { it.isExpanded }
        if(temp >= 0 && temp != position){
            dataList[temp].isExpanded = false
            notifyItemChanged(temp, 0)
        }
    }

    override fun onBindViewHolder(holder: myViewHolder, position: Int, payloads: MutableList<Any>) {
        if(payloads.isNotEmpty() && payloads[0] == 0){
            holder.collapseExpandedView()
        }
        else    super.onBindViewHolder(holder, position, payloads)
    }
}

DiffUtil

DiffUtil is a utility class in Android that helps efficiently update lists in RecyclerView. Instead of reloading the entire list when data changes using the notifyDataSetChanged(), DiffUtil calculates the differences between the old and new lists and updates only the modified items.

Implementing DiffUtil.Callback class :

//MyDiffUtilCallback.kt
class MyDiffUtilCallback( private val oldList: List<MyItem>, 
                          private val newList: List<MyItem> ) : DiffUtil.Callback() {
    override fun getOldListSize() = oldList.size
    override fun getNewListSize() = newList.size
    override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
        return oldList[oldItemPosition].id == newList[newItemPosition].id
    }
    override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
        return oldList[oldItemPosition] == newList[newItemPosition]
    }
}

Applying DiffUtil (Replacing notifyDataSetChanged()) :

val diffResult = DiffUtil.calculateDiff(MyDiffUtilCallback(oldList, newList))
diffResult.dispatchUpdatesTo(adapter)

Implementing DiffUtil using LiveAdapter

ListAdapter is an advanced version of RecyclerView.Adapter that automatically handles efficient updates using DiffUtil. It simplifies list handling by computing differences in the background and updating only the changed items.

  1. Extend ListAdapter instead of RecyclerView.Adapter

     class recyclerAdapter(var context: Context) 
             : ListAdapter<dataRecycler, recyclerAdapter.myViewHolder>(DIFF_CALLBACK){
    
         inner class myViewHolder(var binding : RecyclerviewItemLayoutBinding) 
                     : RecyclerView.ViewHolder(binding.root){
             fun collapseExpandedView(){ binding.statementText.visibility = View.GONE  }
         }
    
         override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): myViewHolder {
             val binding = RecyclerviewItemLayoutBinding.inflate(LayoutInflater.from(context), parent, false)
             return myViewHolder(binding)
         }
    
         override fun onBindViewHolder(holder: myViewHolder, position: Int) {
             val itemBind = getItem(position)
             holder.binding.headingText.text = itemBind.headingString
             holder.binding.descText.text = itemBind.descString
             holder.binding.statementText.text = itemBind.statements
             val isExpanded = itemBind.isExpanded
             holder.binding.statementText.visibility = if(isExpanded) View.VISIBLE 
                                                       else View.GONE
             holder.itemView.setOnClickListener {
                 isAnyExpanded(position)
                 itemBind.isExpanded = !itemBind.isExpanded
                 notifyItemChanged(position)
             }
         }
    
         private fun isAnyExpanded(position: Int){
             val temp = currentList.indexOfFirst { it.isExpanded }
             if(temp >= 0 && temp != position){
                 currentList[temp].isExpanded = false
                 notifyItemChanged(temp, 0)
             }
         }
    
         override fun onBindViewHolder(holder: myViewHolder, position: Int, payloads: MutableList<Any>) {
             if(payloads.isNotEmpty() && payloads[0] == 0){
                 holder.collapseExpandedView()
             }
             else    super.onBindViewHolder(holder, position, payloads)
         }
    
         companion object {
             private val DIFF_CALLBACK = object : DiffUtil.ItemCallback<dataRecycler>() {
                 override fun areItemsTheSame(oldItem: dataRecycler, newItem: dataRecycler): Boolean {
                     return oldItem.id == newItem.id
                 }
                 override fun areContentsTheSame(oldItem: dataRecycler, newItem: dataRecycler): Boolean {
                     return oldItem == newItem
                 }
             }
         }
     }
    
  2. Updating List using submitList()

     class MainActivity : AppCompatActivity(){
         private lateinit var dataList : ArrayList<dataRecycler>
         private lateinit var rvAdapter : recyclerAdapter
         private lateinit var binding: ActivityMainBinding
         override fun onCreate(savedInstanceState: Bundle?) {
             //statements.. 
             dataList= Constant.getData()
             rvAdapter = recyclerAdapter(this)
             binding.recyclerView.layoutManager = LinearLayoutManager(this)
             binding.recyclerView.adapter = rvAdapter
    
             rvAdapter.submitList(dataList)
    
             binding.searchview.setOnQueryTextListener(object : SearchView.OnQueryTextListener{
                 override fun onQueryTextSubmit(p0: String?): Boolean {
                     return false
                 }
                 override fun onQueryTextChange(p0: String?): Boolean {
                     filterList(p0)
                     return true
                 }
             })
         }
    
         private fun filterList(query : String?){
             if(query != null){
                 val filteredList = Constant.getData().filter {
                     it.headingString.lowercase(Locale.ROOT).contains(query.lowercase(Locale.ROOT))
                 }
                 rvAdapter.submitList(filteredList)
             }
         }
     }
    
  • Creating a separate class for DiffUtil when the code is complex, or reuse of it is required.

      class MyAdapter : ListAdapter<MyDataClass, MyAdapter.MyViewHolder>(MyDiffUtil()) { ... }