Android Kotlin 基礎知識 07.2:將 RecyclerView 與 DiffUtil 和資料繫結搭配使用

這個程式碼研究室是 Android Kotlin 基礎知識課程的一部分。如果您按部就班完成程式碼研究室,就能充分體驗到本課程的價值。所有課程程式碼研究室都列在 Android Kotlin 基礎知識程式碼研究室到達網頁

簡介

在先前的程式碼研究室中,您更新了 TrackMySleepQuality 應用程式,以便在 RecyclerView 中顯示睡眠品質資料。您在建構第一個 RecyclerView 時學到的技術,足以應付大多數顯示簡單清單的 RecyclerViews,且清單不會太大。不過,有許多技巧可提升 RecyclerView 對於大型清單的執行效率,並讓程式碼更易於維護,且能為複雜的清單和格線進行擴充。

在本程式碼研究室中,您將以先前程式碼研究室的睡眠追蹤器應用程式為基礎,您將學到更新睡眠資料清單的更有效方法,以及如何搭配 RecyclerView 使用資料繫結。(如果沒有上一個程式碼研究室的應用程式,可以下載本程式碼研究室的範例程式碼)。

必備知識

  • 使用活動、片段和檢視畫面建構基本使用者介面。
  • 在片段之間導覽,以及使用 safeArgs 在片段之間傳遞資料。
  • 查看模型、模型工廠、轉換和 LiveData 及其觀察者。
  • 如何建立 Room 資料庫、建立 DAO,以及定義實體。
  • 如何使用協同程式處理資料庫和其他長時間執行的工作。
  • 如何使用 AdapterViewHolder 和項目版面配置實作基本 RecyclerView

課程內容

  • 如何使用 DiffUtil 有效率地更新 RecyclerView 顯示的清單。
  • 如何搭配 RecyclerView 使用資料繫結。
  • 如何使用繫結轉接器轉換資料。

學習內容

  • 以本系列先前程式碼研究室的 TrackMySleepQuality 應用程式為基礎。
  • 更新 SleepNightAdapter,即可使用 DiffUtil 效率地更新清單。
  • RecyclerView 實作資料繫結,並使用繫結配接器轉換資料。

睡眠追蹤器應用程式有兩個畫面,分別以片段表示,如下圖所示。

左側顯示的第一個畫面有開始和停止追蹤的按鈕。畫面上會顯示部分使用者的睡眠資料。「清除」按鈕會永久刪除應用程式為使用者收集的所有資料。右側的第二個畫面用於選取睡眠品質評分。

這個應用程式的架構是使用 UI 控制器、ViewModelLiveData,以及 Room 資料庫來保存睡眠資料。

睡眠資料會顯示在RecyclerView中。在本程式碼研究室中,您將建構 DiffUtilRecyclerView 的資料繫結部分。完成本程式碼研究室後,應用程式的外觀不會有任何改變,但效率會更高,且更容易擴充及維護。

您可以繼續使用先前程式碼研究室的 SleepTracker 應用程式,也可以從 GitHub 下載 RecyclerViewDiffUtilDataBinding-Starter 應用程式

  1. 如有需要,請從 GitHub 下載 RecyclerViewDiffUtilDataBinding-Starter 應用程式,並在 Android Studio 中開啟專案。
  2. 執行應用程式。
  3. 開啟 SleepNightAdapter.kt 檔案。
  4. 檢查程式碼,熟悉應用程式的結構。請參閱下圖,回顧如何搭配使用 RecyclerView 和轉接器模式,向使用者顯示睡眠資料。

  • 應用程式會根據使用者輸入的內容建立 SleepNight 物件清單。每個 SleepNight 物件代表一晚的睡眠、睡眠時間長度和睡眠品質。
  • SleepNightAdapter 會將 SleepNight 物件清單調整為 RecyclerView 可以使用及顯示的內容。
  • SleepNightAdapter 轉接器會產生 ViewHolders,其中包含檢視區塊、資料和中繼資訊,供回收器檢視區塊顯示資料。
  • RecyclerView 會使用 SleepNightAdapter 判斷要顯示的項目數量 (getItemCount())。RecyclerView 會使用 onCreateViewHolder()onBindViewHolder() 取得繫結至資料的檢視區塊控制代碼,以供顯示。

notifyDataSetChanged() 方法效率不彰

如要告知 RecyclerView 清單中的項目已變更且需要更新,目前的程式碼會在 SleepNightAdapter 中呼叫 notifyDataSetChanged(),如下所示。

var data =  listOf<SleepNight>()
   set(value) {
       field = value
       notifyDataSetChanged()
   }

不過,notifyDataSetChanged() 會告知 RecyclerView 整個清單可能無效。因此,RecyclerView 會重新繫結並重新繪製清單中的每個項目,包括畫面中未顯示的項目。這會造成許多不必要的工作。如果清單很大或很複雜,這個程序可能需要很長時間,導致使用者捲動清單時,畫面閃爍或停頓。

如要修正這個問題,可以明確告知 RecyclerView 變更內容。RecyclerView 接著只會更新畫面上變更的檢視區塊。

RecyclerView 具有豐富的 API,可更新單一元素。您可以使用 notifyItemChanged() 告知 RecyclerView 項目已變更,也可以對新增、移除或移動的項目使用類似函式。您可以手動完成所有工作,但這項工作並不容易,可能需要編寫大量程式碼。

幸好有更好的方法。

DiffUtil 效率高,可為您代勞

RecyclerView 有一個名為 DiffUtil 的類別,用於計算兩個清單之間的差異。DiffUtil 會接收新舊清單,並找出兩者之間的差異。並找出新增、移除或變更的項目。然後使用名為「Eugene W. Myers 差異演算法,找出從舊清單產生新清單所需的最小變更次數。

DiffUtil 找出變更內容後,RecyclerView 就能使用這些資訊,只更新變更、新增、移除或移動的項目,比重新建立整個清單更有效率。

在這項工作中,您會升級 SleepNightAdapter,使用 DiffUtil 針對資料變更最佳化 RecyclerView

步驟 1:實作 SleepNightDiffCallback

如要使用 DiffUtil 類別的功能,請擴充 DiffUtil.ItemCallback

  1. 開啟 SleepNightAdapter.kt
  2. SleepNightAdapter 的完整類別定義下方,建立名為 SleepNightDiffCallback 的新頂層類別,並擴充 DiffUtil.ItemCallback。傳遞 SleepNight 做為泛型參數。
class SleepNightDiffCallback : DiffUtil.ItemCallback<SleepNight>() {
}
  1. 將游標放在 SleepNightDiffCallback 類別名稱中。
  2. 按下 Alt+Enter (Mac 上的 Option+Enter ),然後選取「Implement Members」(實作成員)。
  3. 在開啟的對話方塊中,按住 Shift 鍵並按一下滑鼠左鍵,選取 areItemsTheSame()areContentsTheSame() 方法,然後按一下「OK」

    這會在 SleepNightDiffCallback 內為這兩個方法產生虛設常式,如下所示。DiffUtil 會使用這兩種方法,判斷清單和項目發生哪些變化。
    override fun areItemsTheSame(oldItem: SleepNight, newItem: SleepNight): Boolean {
        TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
    }

    override fun areContentsTheSame(oldItem: SleepNight, newItem: SleepNight): Boolean {
        TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
    }
  1. areItemsTheSame() 內,將 TODO 替換為可測試兩個傳入的 SleepNight 項目 (oldItemnewItem) 是否相同的程式碼。如果項目具有相同的 nightId,則為相同項目,因此請傳回 true。否則,會傳回 falseDiffUtil 會使用這項測試,協助判斷項目是否已新增、移除或移動。
override fun areItemsTheSame(oldItem: SleepNight, newItem: SleepNight): Boolean {
   return oldItem.nightId == newItem.nightId
}
  1. areContentsTheSame() 內,檢查 oldItemnewItem 是否包含相同的資料,也就是兩者是否相等。由於 SleepNight 是資料類別,因此這項等號檢查會檢查所有欄位。Data 類別會自動為您定義 equals 和其他幾個方法。如果 oldItemnewItem 之間有差異,這段程式碼會告知 DiffUtil 項目已更新。
override fun areContentsTheSame(oldItem: SleepNight, newItem: SleepNight): Boolean {
   return oldItem == newItem
}

使用 RecyclerView 顯示會變更的清單是常見模式。RecyclerView 提供轉接程式類別 ListAdapter,可協助您建構以清單為基礎的 RecyclerView 轉接程式。

ListAdapter 會為您追蹤清單,並在清單更新時通知轉接程式。

步驟 1:變更轉接器,以擴充 ListAdapter

  1. SleepNightAdapter.kt 檔案中,變更 SleepNightAdapter 的類別簽名,以擴充 ListAdapter
  2. 如果系統顯示提示,請匯入 androidx.recyclerview.widget.ListAdapter
  3. SleepNightAdapter.ViewHolder 前,將 SleepNight 新增為 ListAdapter 的第一個引數。
  4. SleepNightDiffCallback() 新增為建構函式的參數。ListAdapter 會使用這項參數來偵測清單中的變更內容。完成的 SleepNightAdapter 類別簽章應如下所示。
class SleepNightAdapter : ListAdapter<SleepNight, SleepNightAdapter.ViewHolder>(SleepNightDiffCallback()) {
  1. SleepNightAdapter 類別中,刪除 data 欄位 (包括設定器)。您不再需要這個函式,因為 ListAdapter 會為您追蹤清單。
  2. 刪除 getItemCount() 的覆寫,因為 ListAdapter 會為您實作這個方法。
  3. 如要修正 onBindViewHolder() 中的錯誤,請變更 item 變數。請呼叫 ListAdapter 提供的 getItem(position) 方法,而不是使用 data 取得 item
val item = getItem(position)

步驟 2:使用 submitList() 保持清單更新

程式碼必須在清單變更時通知 ListAdapterListAdapter 提供名為 submitList() 的方法,可告知 ListAdapter 有新版清單。呼叫這個方法時,ListAdapter 會比較新舊清單,並偵測新增、移除、移動或變更的項目。接著,ListAdapter 會更新 RecyclerView 顯示的項目。

  1. 開啟 SleepTrackerFragment.kt
  2. onCreateView()sleepTrackerViewModel 觀察器中,找出參照已刪除 data 變數的錯誤。
  3. adapter.data = it 替換為呼叫 adapter.submitList(it)。更新後的程式碼如下所示。

sleepTrackerViewModel.nights.observe(viewLifecycleOwner, Observer {
   it?.let {
       adapter.submitList(it)
   }
})
  1. 執行應用程式。如果清單很小,您可能不會注意到執行速度變快。

在這項工作中,您會使用與先前程式碼研究室相同的技巧設定資料繫結,並淘汰對 findViewById() 的呼叫。

步驟 1:在版面配置檔案中新增資料繫結

  1. 在「Text」分頁中開啟 list_item_sleep_night.xml 版面配置檔案。
  2. 將游標移至 ConstraintLayout 代碼,然後按下 Alt+Enter 鍵 (Mac 則是按下 Option+Enter 鍵)。意圖選單 (即「快速修正」選單) 隨即開啟。
  3. 選取「Convert to data binding layout」(轉換為資料繫結版面配置)。這會將版面配置包裝到 <layout> 中,並在其中新增 <data> 標記。
  4. 視需要捲動至頂端,並在 <data> 標記內宣告名為 sleep 的變數。
  5. type 設為 SleepNight 的完整名稱 com.example.android.trackmysleepquality.database.SleepNight。完成的 <data> 標記應如下所示。
   <data>
        <variable
            name="sleep"
            type="com.example.android.trackmysleepquality.database.SleepNight"/>
    </data>
  1. 如要強制建立 Binding 物件,請依序選取「Build」>「Clean Project」,然後選取「Build」>「Rebuild Project」。(如果問題仍未解決,請選取「File」>「Invalidate Caches / Restart」)。ListItemSleepNightBinding 繫結物件和相關程式碼會新增至專案產生的檔案。

步驟 2:使用資料繫結擴充項目版面配置

  1. 開啟 SleepNightAdapter.kt
  2. ViewHolder 類別中,找出 from() 方法。
  3. 刪除 view 變數的宣告。

刪除的程式碼:

val view = layoutInflater
       .inflate(R.layout.list_item_sleep_night, parent, false)
  1. view 變數所在位置,定義名為 binding 的新變數,該變數會擴充 ListItemSleepNightBinding 繫結物件,如下所示。匯入繫結物件。
val binding =
ListItemSleepNightBinding.inflate(layoutInflater, parent, false)
  1. 在函式的結尾,傳回 binding,而不是 view
return ViewHolder(binding)
  1. 如要修正錯誤,請將游標放在 binding 字詞上。按下 Alt+Enter 鍵 (在 Mac 上為 Option+Enter 鍵) 開啟意圖選單。
  1. 選取「Change parameter 'itemView' type of primary constructor of class 'ViewHolder' to 'ListItemSleepNightBinding'」(將類別 'ViewHolder' 主要建構函式的參數 'itemView' 類型變更為 'ListItemSleepNightBinding')。這會更新 ViewHolder 類別的參數類型。

  1. 向上捲動至 ViewHolder 的類別定義,即可查看簽章的變更。您會看到 itemView 的錯誤,因為您在 from() 方法中將 itemView 變更為 binding

    ViewHolder 類別定義中,在 itemView 的其中一個例項上按一下滑鼠右鍵,然後依序選取「Refactor」>「Rename」。將名稱變更為 binding
  2. 在建構函式參數 binding 前加上 val,將其設為屬性。
  3. 在父類別 RecyclerView.ViewHolder 的呼叫中,將參數從 binding 變更為 binding.root。您需要傳遞 View,而 binding.root 是項目版面配置中的根 ConstraintLayout
  4. 完成的類別宣告應如以下程式碼所示。
class ViewHolder private constructor(val binding: ListItemSleepNightBinding) : RecyclerView.ViewHolder(binding.root){

您也會看到對 findViewById() 的呼叫發生錯誤,接下來要修正這個問題。

步驟 3:取代 findViewById()

現在可以更新 sleepLengthqualityqualityImage 屬性,改用 binding 物件,而非 findViewById()

  1. sleepLengthqualityStringqualityImage 的初始化設定變更為使用 binding 物件的檢視區塊,如下所示。完成後,程式碼應該就不會再顯示任何錯誤。
val sleepLength: TextView = binding.sleepLength
val quality: TextView = binding.qualityString
val qualityImage: ImageView = binding.qualityImage

有了繫結物件,您就不需要再定義 sleepLengthqualityqualityImage 屬性。DataBinding 會快取查閱作業,因此不需要宣告這些屬性。

  1. sleepLengthqualityqualityImage 屬性名稱上按一下滑鼠右鍵。依序選取「Refactor」>「Inline」,或按下 Control+Command+N 鍵 (Mac 上為 Option+Command+N 鍵)。
  2. 執行應用程式 (如有錯誤,您可能需要清除重建專案)。

在這項工作中,您會升級應用程式,使用資料繫結和繫結轉接器在檢視區塊中設定資料。

在先前的程式碼研究室中,您使用了 Transformations 類別取得 LiveData,並產生格式化字串,以便在文字檢視區塊中顯示。不過,如果需要繫結不同類型或複雜類型,可以提供繫結轉接器,協助資料繫結使用這些類型。繫結轉接器會接收資料,並調整為資料繫結可用於繫結檢視區塊的內容,例如文字或圖片。

您將實作三個繫結介面卡,分別用於品質圖片和每個文字欄位。總而言之,如要宣告繫結轉接器,請定義會採用項目和檢視區塊的方法,並使用 @BindingAdapter 加上註解。在方法主體中,實作轉換。在 Kotlin 中,您可以將繫結介面卡編寫為接收資料的檢視區塊類別擴充功能函式。

步驟 1:建立繫結介面卡

請注意,您必須在這個步驟匯入多門課程,系統不會個別列出。

  1. 開啟 SleepNightAdapater.kt
  2. ViewHolder 類別中,找出 bind() 方法,並回想這個方法的作用。您將取得計算 binding.sleepLengthbinding.qualitybinding.qualityImage 值的程式碼,並在轉接器中使用。(目前程式碼維持不變即可,您會在後續步驟中移動程式碼)。
  3. sleeptracker 套件中,建立並開啟名為 BindingUtils.kt 的檔案。
  4. TextView 上宣告名為 setSleepDurationFormatted 的擴充功能函式,並傳遞 SleepNight。這個函式會是計算及格式化睡眠時間的介面卡。
fun TextView.setSleepDurationFormatted(item: SleepNight) {}
  1. setSleepDurationFormatted 的主體中,將資料繫結至檢視區塊,就像在 ViewHolder.bind() 中一樣。呼叫 convertDurationToFormatted(),然後將 TextViewtext 設為格式化文字。(由於這是 TextView 的擴充功能函式,因此您可以直接存取 text 屬性)。
text = convertDurationToFormatted(item.startTimeMilli, item.endTimeMilli, context.resources)
  1. 如要告知資料繫結這個繫結轉接器,請使用 @BindingAdapter 為函式加上註解。
  2. 這個函式是 sleepDurationFormatted 屬性的轉接程式,因此請將 sleepDurationFormatted 做為引數傳遞至 @BindingAdapter
@BindingAdapter("sleepDurationFormatted")
  1. 第二個介面卡會根據 SleepNight 物件中的值設定睡眠品質。在 TextView 上建立名為 setSleepQualityString() 的擴充功能函式,並傳遞 SleepNight
  2. 在主體中,將資料繫結至檢視區塊,與 ViewHolder.bind() 中的做法相同。呼叫 convertNumericQualityToString 並設定 text
  3. 使用 @BindingAdapter("sleepQualityString") 為函式加上註解。
@BindingAdapter("sleepQualityString")
fun TextView.setSleepQualityString(item: SleepNight) {
   text = convertNumericQualityToString(item.sleepQuality, context.resources)
}
  1. 第三個繫結轉接器會在圖片檢視區塊中設定圖片。在 ImageView 上建立擴充功能函式、呼叫 setSleepImage,並使用 ViewHolder.bind() 中的程式碼,如下所示。
@BindingAdapter("sleepImage")
fun ImageView.setSleepImage(item: SleepNight) {
   setImageResource(when (item.sleepQuality) {
       0 -> R.drawable.ic_sleep_0
       1 -> R.drawable.ic_sleep_1
       2 -> R.drawable.ic_sleep_2
       3 -> R.drawable.ic_sleep_3
       4 -> R.drawable.ic_sleep_4
       5 -> R.drawable.ic_sleep_5
       else -> R.drawable.ic_sleep_active
   })
}

步驟 2:更新 SleepNightAdapter

  1. 開啟 SleepNightAdapter.kt
  2. 刪除 bind() 方法中的所有內容,因為您現在可以使用資料繫結和新介面卡,為您完成這項工作。
fun bind(item: SleepNight) {
}
  1. bind() 中,將休眠狀態指派給 item,因為您需要將新的 SleepNight 告知繫結物件。
binding.sleep = item
  1. 在該行下方新增 binding.executePendingBindings()。這項呼叫會要求資料繫結立即執行任何待處理的繫結,藉此進行最佳化。在 RecyclerView 中使用繫結轉接器時,建議一律呼叫 executePendingBindings(),因為這樣可以稍微加快檢視區塊的大小調整速度。
 binding.executePendingBindings()

步驟 3:將繫結新增至 XML 版面配置

  1. 開啟 list_item_sleep_night.xml
  2. ImageView 中,新增與設定圖片的繫結轉接器同名的 app 屬性。傳遞 sleep 變數,如下所示。

    這個屬性會透過轉接器,在檢視區塊和繫結物件之間建立連線。每當參照 sleepImage 時,轉接程式就會調整 SleepNight 中的資料。
app:sleepImage="@{sleep}"
  1. sleep_lengthquality_string 文字檢視畫面執行相同操作。每當參照 sleepDurationFormattedsleepQualityString 時,轉接程式就會調整 SleepNight 中的資料。
app:sleepDurationFormatted="@{sleep}"
app:sleepQualityString="@{sleep}"
  1. 執行應用程式。應用程式的運作方式與先前完全相同。繫結介面卡會負責格式化及更新檢視區塊的所有工作,簡化 ViewHolder,並讓程式碼結構比以往更完善。

您在過去幾次練習中都顯示相同的清單。這是刻意設計的結果,目的是要向您展示 Adapter 介面可讓您透過多種不同方式架構程式碼。程式碼越複雜,就越需要妥善設計架構。在正式版應用程式中,這些模式和其他模式會與 RecyclerView 一併使用。這些模式都能運作,且各有優點。選擇哪一個取決於您要建構的內容。

恭喜!到目前為止,您已順利邁向精通 Android RecyclerView的道路。

Android Studio 專案:RecyclerViewDiffUtilDataBinding

DiffUtil

  • RecyclerView 有一個名為 DiffUtil 的類別,用於計算兩個清單之間的差異。
  • DiffUtil 有一個名為 ItemCallBack 的類別,您可擴充這個類別,找出兩個清單之間的差異。
  • ItemCallback 類別中,您必須覆寫 areItemsTheSame()areContentsTheSame() 方法。

ListAdapter

  • 如要免費管理清單,可以改用 ListAdapter 類別,而非 RecyclerView.Adapter。不過,如果您使用 ListAdapter,就必須為其他版面配置編寫自己的轉接器,這也是本程式碼研究室要教您的內容。
  • 如要在 Android Studio 中開啟意圖選單,請將游標放在任何程式碼項目上,然後按下 Alt+Enter (Mac 上的 Option+Enter)。這個選單特別適合用於重構程式碼,以及建立實作方法的存根。這個選單會根據內容顯示,因此您必須將游標放在正確位置,才能看到正確的選單。

資料繫結:

  • 在項目版面配置中使用資料繫結,將資料繫結至檢視區塊。

繫結轉接器:

  • 您先前使用 Transformations 從資料建立字串。如要繫結不同或複雜類型的資料,請提供繫結介面卡,協助資料繫結使用這些資料。
  • 如要宣告繫結轉接器,請定義採用項目和檢視區塊的方法,並使用 @BindingAdapter 註解為該方法加上註解。在 Kotlin 中,您可以將繫結轉接器編寫為 View 的擴充功能函式。傳入轉接器要改編的屬性名稱。例如:
@BindingAdapter("sleepDurationFormatted")
  • 在 XML 版面配置中,設定與繫結介面卡同名的 app 屬性。傳遞含有資料的變數。例如:
.app:sleepDurationFormatted="@{sleep}"

Udacity 課程:

Android 開發人員說明文件:

其他資源:

本節列出的作業可由課程講師指派給學習本程式碼研究室的學員。講師可自由採取以下行動:

  • 視需要指派作業。
  • 告知學員如何繳交作業。
  • 為作業評分。

講師可以視需求使用全部或部分建議內容,也可以自由指派任何其他合適的作業。

如果您是自行學習本程式碼研究室,不妨利用這些作業驗收學習成果。

回答問題

第 1 題

下列何者為使用 DiffUtil 的必要條件?請選取所有適用選項。

▢ 擴充 ItemCallBack 類別。

▢ 覆寫 areItemsTheSame()

▢ 覆寫 areContentsTheSame()

▢ 使用資料繫結來追蹤項目之間的差異。

第 2 題

以下有關繫結轉接器的敘述何者正確?

▢ 繫結轉接器是加上 @BindingAdapter 註解的函式。

▢ 使用繫結轉接器可將資料格式從檢視區塊預留位置中分離出來。

▢ 如要使用繫結轉接器,就必須使用 RecyclerViewAdapter

▢ 需要轉換複雜的資料時,繫結轉接器是不錯的解決方案。

第 3 題

什麼時候應考慮使用 Transformations,而非繫結轉接器?請選取所有適用選項。

▢ 您的資料很簡單。

▢ 你要格式化字串。

▢ 你的清單很長。

▢ 您的 ViewHolder 只包含一個檢視畫面。

開始下一個課程:7.3:使用 RecyclerView 搭配 GridLayout