How to Use DiffUtil on RecyclerAdapter
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 theonCreateViewHolder
andonBindViewHolder
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 theonBindViewHolder
function is executed. This function is called when theareItemsTheSame
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.