How to Use DiffUtil on RecyclerAdapter

Barış Keser
Appcent
Published in
4 min readDec 18, 2023

When developing an Android application and in need of a list view, you’ve likely used RecyclerView. However, how did you manage updates to the list? In this article, we will concentrate how to utilize DiffUtil to optimally handle list updates.

What is DiffUtil?

DiffUtil is a class in Android’s RecyclerView framework used to facilitate the detection and management of changes between two datasets.

This class essentially contains two functions:

  • areItemsTheSame(oldItem: T, newItem: T): Boolean: Checks if two items are uniquely identified. If the items are the same, it returns true; otherwise, it returns false. It is generally recommended to use an ID for checking unique equality. If this function returns false, the view for the corresponding item is created from scratch, calling the onCreateViewHolder and onBindViewHolder functions.
  • areContentsTheSame(oldItem: T, newItem: T): Boolean: Checks whether the contents of two items are the same. If the contents are the same, it returns true otherwise it returns false. If it returns false, only the data of the relevant item is re-bound, meaning the view remains the same, and only the onBindViewHolder function is executed. This function is called when the areItemsTheSame function returns true.

Additionally, it has a function named getChangePayload: Any?. This function is called when areItemsTheSame returns true and areContentsTheSame returns false. It calculates changes between two items but updates only the changed field, not the entire data. While doing this, it runs an ItemAnimator. For example, if the background color of an item in the list needs to change when clicked, this function only modifies the background color of the relevant item without concerning itself with other parts.

Let’s start with an example to understand.

Step 1 — Preparing the UI for Items

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content">

<TextView
android:id="@+id/tvName"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginVertical="16dp"
android:layout_marginStart="16dp"
android:textSize="24sp"
android:textStyle="bold"
app:layout_constraintBottom_toTopOf="@id/line"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="1- Sample Name" />

<View
android:id="@+id/line"
android:layout_width="0dp"
android:layout_height="1dp"
android:layout_marginHorizontal="16dp"
android:alpha="0.3"
android:background="@color/black"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintBottom_toBottomOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>

Adım 2 — Adding RecyclerView to an Activity or Fragment

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">

<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rvList"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>

Adım 3 — Creating the Data Class

data class ListItem(
val id: String = UUID.randomUUID().toString(),
val name: String,
val isSelected: Boolean = false
)

Adım 4 — Create the Adapter

class Adapter(
private val clickListener: (item: ListItem) -> Unit
) : RecyclerView.Adapter<Adapter.ViewHolder>() {

private val backgroundPayload = 0

private val differCallBack = object : DiffUtil.ItemCallback<ListItem>() {
override fun areItemsTheSame(oldItem: ListItem, newItem: ListItem): Boolean {
return oldItem.id == newItem.id
}

override fun areContentsTheSame(oldItem: ListItem, newItem: ListItem): Boolean {
return oldItem == newItem
}

override fun getChangePayload(oldItem: ListItem, newItem: ListItem): Any? {
return if (oldItem.isSelected != newItem.isSelected)
backgroundPayload
else super.getChangePayload(oldItem, newItem)
}

}

val differ = AsyncListDiffer(this, differCallBack)

inner class ViewHolder(private val binding: ItemListBinding) :
RecyclerView.ViewHolder(binding.root) {

fun bind(item: ListItem, number: Int) {
updateBackground(item)

binding.tvName.text = "$number- ${item.name}"
binding.root.setOnClickListener {
clickListener.invoke(item)
}
}

fun updateBackground(item: ListItem) {
val backgroundColor = if (item.isSelected) R.color.black else R.color.white
val textColor = if (item.isSelected) R.color.white else R.color.black

binding.root.setBackgroundColor(
ContextCompat.getColor(
binding.root.context,
backgroundColor
)
)
binding.tvName.setTextColor(
ContextCompat.getColor(
binding.tvName.context,
textColor
)
)
}
}

override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
return ViewHolder(
ItemListBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
)
}

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

override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.bind(differ.currentList[position], position + 1)
}

override fun onBindViewHolder(holder: ViewHolder, position: Int, payloads: MutableList<Any>) {
if (payloads.isNotEmpty()) {
if (payloads[0] == backgroundPayload) {
holder.updateBackground(differ.currentList[position])
} else {
super.onBindViewHolder(holder, position, payloads)
}
} else {
super.onBindViewHolder(holder, position, payloads)
}
}
}

Adım 5 — Main Activity

class MainActivity : AppCompatActivity() {

private lateinit var binding: ActivityMainBinding

private var list = listOf(
ListItem(
name = "Sample Name"
),
ListItem(
name = "Sample Name"
),
ListItem(
name = "Sample Name"
),
ListItem(
name = "Sample Name"
),
ListItem(
name = "Sample Name"
),
ListItem(
name = "Sample Name"
),
ListItem(
name = "Sample Name"
),
ListItem(
name = "Sample Name"
),
ListItem(
name = "Sample Name"
),
ListItem(
name = "Sample Name"
)
)

private lateinit var adapter: Adapter

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
adapter = Adapter(::itemClickListener)
binding.rvList.layoutManager = LinearLayoutManager(this)
binding.rvList.adapter = adapter
loadData()
}

private fun loadData() {
adapter.differ.submitList(list)
}

private fun itemClickListener(item: ListItem) {
val newList = list.map {
if(it.id == item.id) {
it.copy(
isSelected = !it.isSelected
)
} else it
}
list = newList
loadData()
}
}

As seen, in MainActivity, we locate the clicked data in the list, toggle the isSelected value, and update the list, providing it to the adapter using DiffUtil. At this point, DiffUtil compares the newly submitted list with the existing one. If the selection status differs for an item, it returns false from the areContentsTheSame function, then checks getChangePayload, which returns the backgroundPayload value. Subsequently, the payload version of onBindViewHolder is triggered, calling the ViewHolder’s updateBackground function.

--

--