如何透過DiffUtil與ListAdapter來更新RecyclerView

GivemepasS
12 min readJan 5, 2022

我們很常使用 RecyclerView 來處理滾動式資料,一開始我們正常的操作都會先宣告一個 RecyclerView。

<?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/list"
android:layout_width="match_parent"
android:layout_height="match_parent" />

</androidx.constraintlayout.widget.ConstraintLayout>

接著宣告一個 Adapter 來佈局。

class DataAdapter : RecyclerView.Adapter<RecyclerView.ViewHolder>() {

private val dataList = arrayListOf<Data>()

@SuppressLint("NotifyDataSetChanged")
fun setData(datas: List<Data>) {
dataList.clear()
dataList.addAll(datas)
notifyDataSetChanged()
}

override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
val inflater = LayoutInflater.from(parent.context)
val view = inflater.inflate(R.layout.data_item_layout, parent, false)
return DataItem(view)
}

override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
(holder as DataItem).bind(dataList[position])
}

override fun getItemCount(): Int = dataList.size

inner class DataItem(private val view: View) : RecyclerView.ViewHolder(view) {

fun bind(data: Data) {
val title = view.findViewById<TextView>(R.id.title)
title.text = data.title
}
}
}

其中裡面的 item layout 佈局如下。

<?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"
android:layout_width="match_parent"
android:layout_height="wrap_content">

<TextView
android:id="@+id/title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:ellipsize="end"
android:maxLines="1"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

最後直接在畫面上宣告相關設定。

class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val recyclerView = findViewById<RecyclerView>(R.id.list)
val manager = LinearLayoutManager(this)
recyclerView.layoutManager = manager
val adapter = DataOriAdapter()
recyclerView.adapter = adapter
val list = arrayListOf<Data>()
for (i in 0..100) {
val data = Data()
data.id = i
data.title = "number $i"
list.add(data)
}
adapter.setData(list)
}
}

這樣就是最初的基礎操作,你就會看到以下畫面。

但是如果只有某幾個項目會頻繁的更新,每次都透過 notifyDataSetChanged 來更新整個 RecyclerView 是相對的浪費,因此,我們可以透過以下幾個方法來進行更新。

notifyItemChanged(position)
notifyItemInserted(position)
notifyItemRemoved(position)

因此,我們可以不需要透過 setData(list : List<Data>) 來刷新整個畫面。

但是如果每一種操作行為都要特別寫一個方法來處理,有時候會覺得很麻煩,此時的救贖就是使用 DiffUtil 來處理,透過 DiffUtil 搭配 ListAdapter 來處理會相對簡單許多。

首先你可以先實作 DiffUtil.ItemCallback 這個 abstract class,可以先來看一下這個介面長怎樣?分別有三個方法。

abstract boolean areItemsTheSame
abstract boolean areContentsTheSame
getChangePayload

其中有兩個方法是 abstract method,也就是說這兩個方法必須實作,分別代表著 Item 的比對,我們設計了一個 class 來處理兩個方法的比對。

class Data {
var id = 0
var title = ""
}

我們來看看這兩個方法怎麼操作?

private class DiffItemCallback : DiffUtil.ItemCallback<Data>() {
override fun areItemsTheSame(oldItem: Data, newItem: Data): Boolean {
return oldItem.id == newItem.id
}

override fun areContentsTheSame(oldItem: Data, newItem: Data): Boolean {
return oldItem.title == newItem.title
}
}

從上面可以看到 areItemsTheSame 是檢查 item 是否是相同,這邊我是設定 id 相同代表他是同一個物件,這代表什麼意思?表示如果 id 是唯一的,那我就可以根據 id 來判斷是同一個物件,而 areContentsTheSame 從字面上的意思就是內容物是否相同。

這兩個差別在哪裡?areItemsTheSame 判斷是否同一個 Item 如果不同就可以理解原始資料是不是新增或刪除了?如果相同則會判斷 areContentsTheSame 是否相同,如果相同則不需要進行畫面更新,如果不同,但是畫面要重新整理。

我先把完整的 Adapter 全部展示出來,後續來一一解釋。

class DataListAdapter : ListAdapter<Data, RecyclerView.ViewHolder>(DiffItemCallback()) {

override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
val inflater = LayoutInflater.from(parent.context)
val view = inflater.inflate(R.layout.data_item_layout, parent, false)
return DataItem(view)
}

override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
(holder as DataItem).bind(getItem(position))
}

override fun getItemCount(): Int = currentList.size

inner class DataItem(private val view: View) : RecyclerView.ViewHolder(view) {
fun bind(data: Data) {
val title = view.findViewById<TextView>(R.id.title)
title.text = data.title
}
}

private class DiffItemCallback : DiffUtil.ItemCallback<Data>() {
override fun areItemsTheSame(oldItem: Data, newItem: Data): Boolean {
return oldItem.id == newItem.id
}

override fun areContentsTheSame(oldItem: Data, newItem: Data): Boolean {
return oldItem.title == newItem.title
}
}
}

可以發現原本繼承的 RecyclerView.Adapter<RecyclerView.ViewHolder>() 變成了 ListAdapter<Data, RecyclerView.ViewHolder>(DiffItemCallback()),此時建構子需要傳入我們剛剛實作的 DiffItemCallback 物件。

而需要覆寫 getItemCount 方法可以透過 currentList 取得 size來回傳。

override fun getItemCount(): Int = currentList.size

如此一來就可以讓 DiffUtil 直接幫我們針對資料變動進行比對。

這邊就可以把 setData 這個方法刪除了,因為不需要自己寫 setData 方法了,可以直接透過 submitList 來進行畫面更新。

adapter.submitList(list)

可以看一下 MainActivity 有什麼改變?

class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val recyclerView = findViewById<RecyclerView>(R.id.list)
val manager = LinearLayoutManager(this)
recyclerView.layoutManager = manager
val adapter = DataListAdapter()
recyclerView.adapter = adapter
val list = arrayListOf<Data>()
for (i in 0..100) {
val data = Data()
data.id = i
data.title = "number $i"
list.add(data)
}
adapter.submitList(list)
}
}

你會看到只有更動最後一行。

adapter.submitList(list)

執行後會發現結果相同,使用者根本感覺不出差異,但是本質上我們程式碼變得簡單許多,而且你會發現,我們只需要對原始資料進行新增、刪除、修改,再對 RecyclerView 進行對應的刷新,就可以更為輕鬆的操作 RecyclerView 了。

最後 完整程式碼 可以透過連結到 GitHub 參考。

--

--