這個程式碼研究室是 Android Kotlin 基礎知識課程的一部分。如果您按部就班完成程式碼研究室,就能充分體驗到本課程的價值。所有課程程式碼研究室都列在 Android Kotlin 基礎知識程式碼研究室到達網頁。
簡介
在先前的程式碼研究室中,您更新了 TrackMySleepQuality 應用程式,以便在 RecyclerView
中顯示睡眠品質資料。您在建構第一個 RecyclerView
時學到的技術,足以應付大多數顯示簡單清單的 RecyclerViews
,且清單不會太大。不過,有許多技巧可提升 RecyclerView
對於大型清單的執行效率,並讓程式碼更易於維護,且能為複雜的清單和格線進行擴充。
在本程式碼研究室中,您將以先前程式碼研究室的睡眠追蹤器應用程式為基礎,您將學到更新睡眠資料清單的更有效方法,以及如何搭配 RecyclerView
使用資料繫結。(如果沒有上一個程式碼研究室的應用程式,可以下載本程式碼研究室的範例程式碼)。
必備知識
- 使用活動、片段和檢視畫面建構基本使用者介面。
- 在片段之間導覽,以及使用
safeArgs
在片段之間傳遞資料。 - 查看模型、模型工廠、轉換和
LiveData
及其觀察者。 - 如何建立
Room
資料庫、建立 DAO,以及定義實體。 - 如何使用協同程式處理資料庫和其他長時間執行的工作。
- 如何使用
Adapter
、ViewHolder
和項目版面配置實作基本RecyclerView
。
課程內容
- 如何使用
DiffUtil
有效率地更新RecyclerView
顯示的清單。 - 如何搭配
RecyclerView
使用資料繫結。 - 如何使用繫結轉接器轉換資料。
學習內容
- 以本系列先前程式碼研究室的 TrackMySleepQuality 應用程式為基礎。
- 更新
SleepNightAdapter
,即可使用DiffUtil
效率地更新清單。 - 為
RecyclerView
實作資料繫結,並使用繫結配接器轉換資料。
睡眠追蹤器應用程式有兩個畫面,分別以片段表示,如下圖所示。
左側顯示的第一個畫面有開始和停止追蹤的按鈕。畫面上會顯示部分使用者的睡眠資料。「清除」按鈕會永久刪除應用程式為使用者收集的所有資料。右側的第二個畫面用於選取睡眠品質評分。
這個應用程式的架構是使用 UI 控制器、ViewModel
和 LiveData
,以及 Room
資料庫來保存睡眠資料。
睡眠資料會顯示在RecyclerView
中。在本程式碼研究室中,您將建構 DiffUtil
和 RecyclerView
的資料繫結部分。完成本程式碼研究室後,應用程式的外觀不會有任何改變,但效率會更高,且更容易擴充及維護。
您可以繼續使用先前程式碼研究室的 SleepTracker 應用程式,也可以從 GitHub 下載 RecyclerViewDiffUtilDataBinding-Starter 應用程式。
- 如有需要,請從 GitHub 下載 RecyclerViewDiffUtilDataBinding-Starter 應用程式,並在 Android Studio 中開啟專案。
- 執行應用程式。
- 開啟
SleepNightAdapter.kt
檔案。 - 檢查程式碼,熟悉應用程式的結構。請參閱下圖,回顧如何搭配使用
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
。
- 開啟
SleepNightAdapter.kt
。 - 在
SleepNightAdapter
的完整類別定義下方,建立名為SleepNightDiffCallback
的新頂層類別,並擴充DiffUtil.ItemCallback
。傳遞SleepNight
做為泛型參數。
class SleepNightDiffCallback : DiffUtil.ItemCallback<SleepNight>() {
}
- 將游標放在
SleepNightDiffCallback
類別名稱中。 - 按下
Alt+Enter
(Mac 上的Option+Enter
),然後選取「Implement Members」(實作成員)。 - 在開啟的對話方塊中,按住 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.
}
- 在
areItemsTheSame()
內,將TODO
替換為可測試兩個傳入的SleepNight
項目 (oldItem
和newItem
) 是否相同的程式碼。如果項目具有相同的nightId
,則為相同項目,因此請傳回true
。否則,會傳回false
。DiffUtil
會使用這項測試,協助判斷項目是否已新增、移除或移動。
override fun areItemsTheSame(oldItem: SleepNight, newItem: SleepNight): Boolean {
return oldItem.nightId == newItem.nightId
}
- 在
areContentsTheSame()
內,檢查oldItem
和newItem
是否包含相同的資料,也就是兩者是否相等。由於SleepNight
是資料類別,因此這項等號檢查會檢查所有欄位。Data
類別會自動為您定義equals
和其他幾個方法。如果oldItem
和newItem
之間有差異,這段程式碼會告知DiffUtil
項目已更新。
override fun areContentsTheSame(oldItem: SleepNight, newItem: SleepNight): Boolean {
return oldItem == newItem
}
使用 RecyclerView
顯示會變更的清單是常見模式。RecyclerView
提供轉接程式類別 ListAdapter
,可協助您建構以清單為基礎的 RecyclerView
轉接程式。
ListAdapter
會為您追蹤清單,並在清單更新時通知轉接程式。
步驟 1:變更轉接器,以擴充 ListAdapter
- 在
SleepNightAdapter.kt
檔案中,變更SleepNightAdapter
的類別簽名,以擴充ListAdapter
。 - 如果系統顯示提示,請匯入
androidx.recyclerview.widget.ListAdapter
。 - 在
SleepNightAdapter.ViewHolder
前,將SleepNight
新增為ListAdapter
的第一個引數。 - 將
SleepNightDiffCallback()
新增為建構函式的參數。ListAdapter
會使用這項參數來偵測清單中的變更內容。完成的SleepNightAdapter
類別簽章應如下所示。
class SleepNightAdapter : ListAdapter<SleepNight, SleepNightAdapter.ViewHolder>(SleepNightDiffCallback()) {
- 在
SleepNightAdapter
類別中,刪除data
欄位 (包括設定器)。您不再需要這個函式,因為ListAdapter
會為您追蹤清單。 - 刪除
getItemCount()
的覆寫,因為ListAdapter
會為您實作這個方法。 - 如要修正
onBindViewHolder()
中的錯誤,請變更item
變數。請呼叫ListAdapter
提供的getItem(position)
方法,而不是使用data
取得item
。
val item = getItem(position)
步驟 2:使用 submitList() 保持清單更新
程式碼必須在清單變更時通知 ListAdapter
。ListAdapter
提供名為 submitList()
的方法,可告知 ListAdapter
有新版清單。呼叫這個方法時,ListAdapter
會比較新舊清單,並偵測新增、移除、移動或變更的項目。接著,ListAdapter
會更新 RecyclerView
顯示的項目。
- 開啟
SleepTrackerFragment.kt
。 - 在
onCreateView()
的sleepTrackerViewModel
觀察器中,找出參照已刪除data
變數的錯誤。 - 將
adapter.data = it
替換為呼叫adapter.submitList(it)
。更新後的程式碼如下所示。
sleepTrackerViewModel.nights.observe(viewLifecycleOwner, Observer {
it?.let {
adapter.submitList(it)
}
})
- 執行應用程式。如果清單很小,您可能不會注意到執行速度變快。
在這項工作中,您會使用與先前程式碼研究室相同的技巧設定資料繫結,並淘汰對 findViewById()
的呼叫。
步驟 1:在版面配置檔案中新增資料繫結
- 在「Text」分頁中開啟
list_item_sleep_night.xml
版面配置檔案。 - 將游標移至
ConstraintLayout
代碼,然後按下Alt+Enter
鍵 (Mac 則是按下Option+Enter
鍵)。意圖選單 (即「快速修正」選單) 隨即開啟。 - 選取「Convert to data binding layout」(轉換為資料繫結版面配置)。這會將版面配置包裝到
<layout>
中,並在其中新增<data>
標記。 - 視需要捲動至頂端,並在
<data>
標記內宣告名為sleep
的變數。 - 將
type
設為SleepNight
的完整名稱com.example.android.trackmysleepquality.database.SleepNight
。完成的<data>
標記應如下所示。
<data>
<variable
name="sleep"
type="com.example.android.trackmysleepquality.database.SleepNight"/>
</data>
- 如要強制建立
Binding
物件,請依序選取「Build」>「Clean Project」,然後選取「Build」>「Rebuild Project」。(如果問題仍未解決,請選取「File」>「Invalidate Caches / Restart」)。ListItemSleepNightBinding
繫結物件和相關程式碼會新增至專案產生的檔案。
步驟 2:使用資料繫結擴充項目版面配置
- 開啟
SleepNightAdapter.kt
。 - 在
ViewHolder
類別中,找出from()
方法。 - 刪除
view
變數的宣告。
要刪除的程式碼:
val view = layoutInflater
.inflate(R.layout.list_item_sleep_night, parent, false)
- 在
view
變數所在位置,定義名為binding
的新變數,該變數會擴充ListItemSleepNightBinding
繫結物件,如下所示。匯入繫結物件。
val binding =
ListItemSleepNightBinding.inflate(layoutInflater, parent, false)
- 在函式的結尾,傳回
binding
,而不是view
。
return ViewHolder(binding)
- 如要修正錯誤,請將游標放在
binding
字詞上。按下Alt+Enter
鍵 (在 Mac 上為Option+Enter
鍵) 開啟意圖選單。
- 選取「Change parameter 'itemView' type of primary constructor of class 'ViewHolder' to 'ListItemSleepNightBinding'」(將類別 'ViewHolder' 主要建構函式的參數 'itemView' 類型變更為 'ListItemSleepNightBinding')。這會更新
ViewHolder
類別的參數類型。
- 向上捲動至
ViewHolder
的類別定義,即可查看簽章的變更。您會看到itemView
的錯誤,因為您在from()
方法中將itemView
變更為binding
。
在ViewHolder
類別定義中,在itemView
的其中一個例項上按一下滑鼠右鍵,然後依序選取「Refactor」>「Rename」。將名稱變更為binding
。 - 在建構函式參數
binding
前加上val
,將其設為屬性。 - 在父類別
RecyclerView.ViewHolder
的呼叫中,將參數從binding
變更為binding.root
。您需要傳遞View
,而binding.root
是項目版面配置中的根ConstraintLayout
。 - 完成的類別宣告應如以下程式碼所示。
class ViewHolder private constructor(val binding: ListItemSleepNightBinding) : RecyclerView.ViewHolder(binding.root){
您也會看到對 findViewById()
的呼叫發生錯誤,接下來要修正這個問題。
步驟 3:取代 findViewById()
現在可以更新 sleepLength
、quality
和 qualityImage
屬性,改用 binding
物件,而非 findViewById()
。
- 將
sleepLength
、qualityString
和qualityImage
的初始化設定變更為使用binding
物件的檢視區塊,如下所示。完成後,程式碼應該就不會再顯示任何錯誤。
val sleepLength: TextView = binding.sleepLength
val quality: TextView = binding.qualityString
val qualityImage: ImageView = binding.qualityImage
有了繫結物件,您就不需要再定義 sleepLength
、quality
和 qualityImage
屬性。DataBinding
會快取查閱作業,因此不需要宣告這些屬性。
- 在
sleepLength
、quality
和qualityImage
屬性名稱上按一下滑鼠右鍵。依序選取「Refactor」>「Inline」,或按下Control+Command+N
鍵 (Mac 上為Option+Command+N
鍵)。 - 執行應用程式 (如有錯誤,您可能需要清除並重建專案)。
在這項工作中,您會升級應用程式,使用資料繫結和繫結轉接器在檢視區塊中設定資料。
在先前的程式碼研究室中,您使用了 Transformations
類別取得 LiveData
,並產生格式化字串,以便在文字檢視區塊中顯示。不過,如果需要繫結不同類型或複雜類型,可以提供繫結轉接器,協助資料繫結使用這些類型。繫結轉接器會接收資料,並調整為資料繫結可用於繫結檢視區塊的內容,例如文字或圖片。
您將實作三個繫結介面卡,分別用於品質圖片和每個文字欄位。總而言之,如要宣告繫結轉接器,請定義會採用項目和檢視區塊的方法,並使用 @BindingAdapter
加上註解。在方法主體中,實作轉換。在 Kotlin 中,您可以將繫結介面卡編寫為接收資料的檢視區塊類別擴充功能函式。
步驟 1:建立繫結介面卡
請注意,您必須在這個步驟匯入多門課程,系統不會個別列出。
- 開啟
SleepNightAdapater.kt
。 - 在
ViewHolder
類別中,找出bind()
方法,並回想這個方法的作用。您將取得計算binding.sleepLength
、binding.quality
和binding.qualityImage
值的程式碼,並在轉接器中使用。(目前程式碼維持不變即可,您會在後續步驟中移動程式碼)。 - 在
sleeptracker
套件中,建立並開啟名為BindingUtils.kt
的檔案。 - 在
TextView
上宣告名為setSleepDurationFormatted
的擴充功能函式,並傳遞SleepNight
。這個函式會是計算及格式化睡眠時間的介面卡。
fun TextView.setSleepDurationFormatted(item: SleepNight) {}
- 在
setSleepDurationFormatted
的主體中,將資料繫結至檢視區塊,就像在ViewHolder.bind()
中一樣。呼叫convertDurationToFormatted()
,然後將TextView
的text
設為格式化文字。(由於這是TextView
的擴充功能函式,因此您可以直接存取text
屬性)。
text = convertDurationToFormatted(item.startTimeMilli, item.endTimeMilli, context.resources)
- 如要告知資料繫結這個繫結轉接器,請使用
@BindingAdapter
為函式加上註解。 - 這個函式是
sleepDurationFormatted
屬性的轉接程式,因此請將sleepDurationFormatted
做為引數傳遞至@BindingAdapter
。
@BindingAdapter("sleepDurationFormatted")
- 第二個介面卡會根據
SleepNight
物件中的值設定睡眠品質。在TextView
上建立名為setSleepQualityString()
的擴充功能函式,並傳遞SleepNight
。 - 在主體中,將資料繫結至檢視區塊,與
ViewHolder.bind()
中的做法相同。呼叫convertNumericQualityToString
並設定text
。 - 使用
@BindingAdapter("sleepQualityString")
為函式加上註解。
@BindingAdapter("sleepQualityString")
fun TextView.setSleepQualityString(item: SleepNight) {
text = convertNumericQualityToString(item.sleepQuality, context.resources)
}
- 第三個繫結轉接器會在圖片檢視區塊中設定圖片。在
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
- 開啟
SleepNightAdapter.kt
。 - 刪除
bind()
方法中的所有內容,因為您現在可以使用資料繫結和新介面卡,為您完成這項工作。
fun bind(item: SleepNight) {
}
- 在
bind()
中,將休眠狀態指派給item
,因為您需要將新的SleepNight
告知繫結物件。
binding.sleep = item
- 在該行下方新增
binding.executePendingBindings()
。這項呼叫會要求資料繫結立即執行任何待處理的繫結,藉此進行最佳化。在RecyclerView
中使用繫結轉接器時,建議一律呼叫executePendingBindings()
,因為這樣可以稍微加快檢視區塊的大小調整速度。
binding.executePendingBindings()
步驟 3:將繫結新增至 XML 版面配置
- 開啟
list_item_sleep_night.xml
。 - 在
ImageView
中,新增與設定圖片的繫結轉接器同名的app
屬性。傳遞sleep
變數,如下所示。
這個屬性會透過轉接器,在檢視區塊和繫結物件之間建立連線。每當參照sleepImage
時,轉接程式就會調整SleepNight
中的資料。
app:sleepImage="@{sleep}"
- 對
sleep_length
和quality_string
文字檢視畫面執行相同操作。每當參照sleepDurationFormatted
或sleepQualityString
時,轉接程式就會調整SleepNight
中的資料。
app:sleepDurationFormatted="@{sleep}"
app:sleepQualityString="@{sleep}"
- 執行應用程式。應用程式的運作方式與先前完全相同。繫結介面卡會負責格式化及更新檢視區塊的所有工作,簡化
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
只包含一個檢視畫面。