這個程式碼研究室是 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 只包含一個檢視畫面。

