Android Kotlin 基礎課程 07.2:與 RecyclerView 之間的資料差異和資料繫結

本程式碼研究室是 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。在這個程式碼研究室中,您需要為 RecyclerView 建構 DiffUtil 和資料繫結部分。完成這個程式碼研究室後,您的應用程式外觀會完全相同,但將更有效率,更容易進行擴充和維護。

您可以繼續使用先前的程式碼研究室提供的 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()」取得繫結的資料顯示對象。

NotificationsDataSetChanged() 方法的效率較低

如要通知 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」找出變更項目後,可以使用該資訊只更新已變更、新增、移除或移動的項目,這比重做整份清單更有效率。

在這項工作中,您會將 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 )然後選取執行成據
  3. 在隨即開啟的對話方塊中,按下 shift-left-click 以選取 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。如果沒有,則傳回 false。「DiffUtil」會使用這項測試作業來確認該項目是否已新增、移除或移動。
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 欄位 (包括 setter)。「ListAdapter」已經為您追蹤這個清單了,您不再需要這項功能。
  2. 刪除 getItemCount() 的覆寫值,因為 ListAdapter 會為您導入這個方法。
  3. 如要清除 onBindViewHolder() 中的錯誤,請變更 item 變數。與其使用 data 取得 item,請呼叫 ListAdapter 提供的 getItem(position) 方法。
val item = getItem(position)

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

有變更清單時,您的程式碼必須通知 ListAdapterListAdapter 提供了名為 submitList() 的方法,可告知 ListAdapter 有新的清單可用。呼叫這個方法時,ListAdapter 會將新清單與舊清單區分,並偵測新增、移除、移動或變更的項目。然後 ListAdapter 會更新 RecyclerView 顯示的項目。

  1. 開啟 SleepTrackerFragment.kt
  2. onCreateView() 的觀測器中,找出參照了 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. 選取 [轉換為資料繫結版面配置]。這麼做會將版面配置包裝成 <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 物件,請選取 [Create > Clean Project] (建構專案;清理專案),然後選取 [Create > Rebuild Project] (建構專案;重新建構專案)。(如果問題仍無法解決,請選取 [檔案] > [撤銷快取 / 重新啟動])。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. 選擇變更參數 'itemView' 類別 'ViewHolder' 的主要建構函式類型 — 至 'ListItemSleepNightbinding'這會更新 ViewHolder 類別的參數類型。

  1. 捲動至「ViewHolder」的課程定義,即可在簽名中查看變更。您看到 itemView 的錯誤,因為您在 from() 方法中將 itemView 變更為 binding

    ViewHolder 類別定義中,請在 itemView 的任一出現右鍵上按一下滑鼠右鍵,然後選取 [Re 因素] > [重新命名]。將名稱變更為「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 屬性名稱上按一下滑鼠右鍵。選取 [重構 > 內嵌],或按下 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}"

大學課程:

Android 開發人員說明文件:

其他資源:

這個部分會列出在代碼研究室中,受老師主導的課程作業的可能學生作業。由老師自行決定要執行下列動作:

  • 視需要指派家庭作業。
  • 告知學生如何提交家庭作業。
  • 批改家庭作業。

老師可視需要使用這些建議,並視情況指派其他合適的家庭作業。

如果您是自行操作本程式碼研究室,歡迎透過這些家庭作業來測試自己的知識。

回答這些問題

第 1 題

以下何者是使用 DiffUtil 的?請選取所有適用選項。

▢ 擴充 ItemCallBack 類別。

▢ 覆寫 areItemsTheSame()

▢ 覆寫 areContentsTheSame()

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

問題 2

關於繫結轉接器,下列敘述何者正確?

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

▢ 使用繫結轉接程式可將資料格式與檢視畫面的分隔符號分開。

▢ 如要使用繫結轉接程式,您必須使用 RecyclerViewAdapter

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

問題 3

何時該使用 Transformations 取代繫結轉接器?請選取所有適用選項。

▢ 資料十分簡單。

▢ 您正在設定字串格式。

▢ 清單過長。

▢ 您的「ViewHolder」中只有一個資料檢視。

開始下一門課程:7.3:使用 RecyclerView 的 GridLayout