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 :
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.
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.
Data: Here, we use an Adapter class that binds our data with the ViewHolder views.
Key Classes used in Creating RecyclerView in Android :
RecyclerView
which is a ViewGroup Class.A Class extending the
RecyclerView.ViewHolder
class, as our ViewHolder object, defines how each element looks.A Class extending the
RecyclerView.Adapter
class, that binds the view with their data.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
- 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
}
- 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()
}
}
- Set the Click Listener in
onBindViewHolder
: Assign theonClick
action to the relevant views in theRecyclerView
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)
}
}
}
- 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
orVERTICAL
): Determines the direction of the layout.ReverseLayout (
true
orfalse
): Iftrue
, 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
- 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>
- 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
- Modifying the
dataRecycler.kt
andrecyclerview_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>
- 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.
Extend
ListAdapter
instead ofRecyclerView.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 } } } }
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()) { ... }