這個程式碼研究室是 Android Kotlin 基礎知識課程的一部分。如果您按部就班完成程式碼研究室,就能充分體驗到本課程的價值。所有課程程式碼研究室都列在 Android Kotlin 基礎知識程式碼研究室到達網頁。
簡介
在本程式碼研究室中,您將瞭解如何新增標題,使其跨越 RecyclerView
中顯示的清單寬度。您將以先前程式碼研究室的睡眠追蹤器應用程式為基礎。
必備知識
- 如何使用活動、片段和檢視畫面建構基本使用者介面。
- 如何瀏覽各個片段之間,以及如何使用
safeArgs
在片段之間傳遞資料。 - 查看模型、模型工廠、轉換和
LiveData
及其觀察者。 - 如何建立
Room
資料庫、建立 DAO,以及定義實體。 - 如何使用協同程式進行資料庫互動,以及處理其他長時間執行的工作。
- 如何使用
Adapter
、ViewHolder
和項目版面配置實作基本RecyclerView
。 - 如何為
RecyclerView
實作資料繫結。 - 如何建立及使用繫結轉接程式來轉換資料。
- 如何使用
GridLayoutManager
。 - 如何擷取及處理
RecyclerView.
中項目的點擊事件
課程內容
- 如何搭配
RecyclerView
使用多個ViewHolder
,新增不同版面配置的項目。具體來說,就是如何使用第二個ViewHolder
,在RecyclerView
中顯示的項目上方新增標題。
學習內容
- 以本系列先前程式碼研究室的 TrackMySleepQuality 應用程式為基礎,
- 在
RecyclerView
中顯示的睡眠夜數上方,新增橫跨整個螢幕寬度的標題。
您一開始使用的睡眠追蹤器應用程式有三個畫面,以片段表示,如下圖所示。
左側顯示的第一個畫面有開始和停止追蹤的按鈕。畫面上會顯示部分使用者的睡眠資料。「清除」按鈕會永久刪除應用程式為使用者收集的所有資料。中間的第二個畫面用於選取睡眠品質評分。第三個畫面是詳細資料檢視畫面,使用者輕觸格線中的項目時就會開啟。
這個應用程式採用簡化架構,包含 UI 控制器、檢視模型和 LiveData
,以及用於保存睡眠資料的 Room
資料庫。
在本程式碼研究室中,您會在顯示的項目格線中新增標題。最終的主畫面如下所示:
本程式碼研究室將說明在 RecyclerView
中加入使用不同版面配置項目的基本原則。常見的例子是在清單或格線中加入標題。清單可以有一個標題,用來描述項目內容。清單也可以有多個標題,用於分組及分隔單一清單中的項目。
RecyclerView
不會知道您的資料,也不會知道每個項目的版面配置類型。LayoutManager
會在畫面上排列項目,但轉接器會調整要顯示的資料,並將檢視區塊控制代碼傳遞至 RecyclerView
。因此您會在轉接器中新增程式碼來建立標頭。
新增標題的兩種方式
在 RecyclerView
中,清單中的每個項目都對應到從 0 開始的索引編號。例如:
[實際資料] -> [介面卡檢視畫面]
[0: SleepNight] -> [0: SleepNight]
[1: SleepNight] -> [1: SleepNight]
[2: SleepNight] -> [2: SleepNight]
如要將標頭加入清單,其中一種方法是修改介面卡,透過檢查標頭顯示位置的索引,使用不同的 ViewHolder
。Adapter
會負責追蹤標頭。舉例來說,如要在表格頂端顯示標題,您需要在版面配置以零為索引的項目時,傳回標題的 ViewHolder
。然後,所有其他項目都會對應至標題偏移,如下所示。
[實際資料] -> [介面卡檢視畫面]
[0: Header]
[0: SleepNight] -> [1: SleepNight]
[1: SleepNight] -> [2: SleepNight]
[2: SleepNight] -> [3: SleepNight.
如要新增標題,也可以修改資料格的支援資料集。由於需要顯示的所有資料都儲存在清單中,因此您可以修改清單,加入代表標題的項目。這種做法較容易理解,但您必須思考如何設計物件,才能將不同項目類型合併為單一清單。以這種方式實作後,轉接器就會顯示傳遞給它的項目。因此位置 0 的項目是標題,位置 1 的項目是 SleepNight
,直接對應到畫面上的內容。
[實際資料] -> [介面卡檢視畫面]
[0: Header] -> [0: Header]
[1: SleepNight] -> [1: SleepNight]
[2: SleepNight] -> [2: SleepNight]
[3: SleepNight] -> [3: SleepNight]
每種方法都有優缺點。變更資料集不會對其餘的轉接器程式碼造成太大影響,而且您可以透過操控資料清單來新增標頭邏輯。另一方面,透過檢查標頭的索引使用不同的 ViewHolder
,可讓您更自由地設定標頭版面配置。此外,轉接器也能處理資料如何調整至檢視畫面,而不需修改支援資料。
在本程式碼研究室中,您會更新 RecyclerView
,在清單開頭顯示標題。在這種情況下,應用程式會使用不同的 ViewHolder
做為標頭和資料項目的標頭。應用程式會檢查清單的索引,判斷要使用哪個 ViewHolder
。
步驟 1:建立 DataItem 類別
如要將項目類型抽象化,讓轉接器只處理「項目」,您可以建立資料持有者類別,代表 SleepNight
或 Header
。資料集隨後會成為資料持有者項目清單。
您可以從 GitHub 取得範例應用程式,也可以繼續使用您在先前的程式碼研究室中建構的 SleepTracker 應用程式。
- 從 GitHub 下載 RecyclerViewHeaders-Starter 程式碼。RecyclerViewHeaders-Starter 目錄包含本程式碼研究室所需的 SleepTracker 應用程式入門版本。您也可以選擇繼續使用先前程式碼研究室中完成的應用程式。
- 開啟「SleepNightAdapter.kt」SleepNightAdapter.kt。
- 在
SleepNightListener
類別下方,於頂層定義名為DataItem
的sealed
類別,代表資料項目。sealed
類別會定義封閉型別,也就是說,DataItem
的所有子類別都必須在這個檔案中定義。因此,編譯器會知道子類別的數量。程式碼的其他部分無法定義可能導致配接器中斷的新型DataItem
。
sealed class DataItem {
}
- 在
DataItem
類別的主體中,定義兩個類別,分別代表不同類型的資料項目。第一個是SleepNightItem
,這是SleepNight
的包裝函式,因此會採用名為sleepNight
的單一值。如要將其設為密封類別的一部分,請擴充DataItem
。
data class SleepNightItem(val sleepNight: SleepNight): DataItem()
- 第二個類別是
Header
,代表標題。由於標頭沒有實際資料,因此可以宣告為object
。也就是說,只會有一個Header
執行個體。再次將其擴充為DataItem
。
object Header: DataItem()
- 在
DataItem
內,於類別層級定義名為id
的abstract
Long
屬性。當配接器使用DiffUtil
判斷項目是否及如何變更時,DiffItemCallback
需要知道每個項目的 ID。您會看到錯誤訊息,因為SleepNightItem
和Header
需要覆寫抽象屬性id
。
abstract val id: Long
- 在
SleepNightItem
中,覆寫id
以傳回nightId
。
override val id = sleepNight.nightId
- 在
Header
中,覆寫id
以傳回Long.MIN_VALUE
,這是非常小的數字 (實際上是 -2 的 63 次方)。因此,這項設定絕不會與現有的任何nightId
發生衝突。
override val id = Long.MIN_VALUE
- 完成的程式碼應如下所示,且應用程式應能順利建構。
sealed class DataItem {
abstract val id: Long
data class SleepNightItem(val sleepNight: SleepNight): DataItem() {
override val id = sleepNight.nightId
}
object Header: DataItem() {
override val id = Long.MIN_VALUE
}
}
步驟 2:為標題建立 ViewHolder
- 在名為 header.xml 的新版面配置資源檔案中,建立標題的版面配置,顯示
TextView
。這部分沒有什麼特別之處,因此我們直接提供程式碼。
<?xml version="1.0" encoding="utf-8"?>
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textAppearance="?android:attr/textAppearanceLarge"
android:text="Sleep Results"
android:padding="8dp" />
- 將
"Sleep Results"
擷取為字串資源,並命名為header_text
。
<string name="header_text">Sleep Results</string>
- 在 SleepNightAdapter.kt 的
SleepNightAdapter
內,於ViewHolder
類別上方建立新的TextViewHolder
類別。這個類別會膨脹 textview.xml 版面配置,並傳回TextViewHolder
例項。由於您先前已完成這項操作,因此這裡提供程式碼,您必須匯入View
和R
:
class TextViewHolder(view: View): RecyclerView.ViewHolder(view) {
companion object {
fun from(parent: ViewGroup): TextViewHolder {
val layoutInflater = LayoutInflater.from(parent.context)
val view = layoutInflater.inflate(R.layout.header, parent, false)
return TextViewHolder(view)
}
}
}
步驟 3:更新 SleepNightAdapter
接著,您需要更新 SleepNightAdapter
的聲明。它必須能夠使用任何類型的檢視畫面持有者,而不僅支援一種 ViewHolder
。
定義項目類型
- 在
SleepNightAdapter.kt
的頂層,於import
陳述式下方和SleepNightAdapter
上方,定義兩個檢視區塊類型的常數。RecyclerView
必須區分每個項目的檢視類型,才能正確指派檢視容器。
private val ITEM_VIEW_TYPE_HEADER = 0
private val ITEM_VIEW_TYPE_ITEM = 1
- 在
SleepNightAdapter
內,建立函式來覆寫getItemViewType()
,根據目前項目的類型傳回正確的標題或項目常數。
override fun getItemViewType(position: Int): Int {
return when (getItem(position)) {
is DataItem.Header -> ITEM_VIEW_TYPE_HEADER
is DataItem.SleepNightItem -> ITEM_VIEW_TYPE_ITEM
}
}
更新 SleepNightAdapter 定義
- 在
SleepNightAdapter
的定義中,將ListAdapter
的第一個引數從SleepNight
更新為DataItem
。 - 在
SleepNightAdapter
的定義中,將ListAdapter
的第二個泛型引數從SleepNightAdapter.ViewHolder
變更為RecyclerView.ViewHolder
。您會看到必要更新的一些錯誤,且類別標頭應如下所示。
class SleepNightAdapter(val clickListener: SleepNightListener):
ListAdapter<DataItem, RecyclerView.ViewHolder>(SleepNightDiffCallback()) {
更新 onCreateViewHolder()
- 變更
onCreateViewHolder()
的簽名,以傳回RecyclerView.ViewHolder
。
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder
- 擴展
onCreateViewHolder()
方法的實作方式,針對每個項目類型測試並傳回適當的檢視區塊持有者。更新後的方法應如下所示。
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
return when (viewType) {
ITEM_VIEW_TYPE_HEADER -> TextViewHolder.from(parent)
ITEM_VIEW_TYPE_ITEM -> ViewHolder.from(parent)
else -> throw ClassCastException("Unknown viewType ${viewType}")
}
}
更新 onBindViewHolder()
- 將
onBindViewHolder()
的參數類型從ViewHolder
變更為RecyclerView.ViewHolder
。
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int)
- 新增條件,只在檢視區塊持有者為
ViewHolder
時,才將資料指派給該持有者。
when (holder) {
is ViewHolder -> {...}
- 將
getItem()
傳回的物件類型轉換為DataItem.SleepNightItem
。完成的onBindViewHolder()
函式應如下所示。
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
when (holder) {
is ViewHolder -> {
val nightItem = getItem(position) as DataItem.SleepNightItem
holder.bind(nightItem.sleepNight, clickListener)
}
}
}
更新 DiffUtil 回呼
- 變更
SleepNightDiffCallback
中的方法,改用新的DataItem
類別,而非SleepNight
。如以下程式碼所示,禁止顯示 Lint 警告。
class SleepNightDiffCallback : DiffUtil.ItemCallback<DataItem>() {
override fun areItemsTheSame(oldItem: DataItem, newItem: DataItem): Boolean {
return oldItem.id == newItem.id
}
@SuppressLint("DiffUtilEquals")
override fun areContentsTheSame(oldItem: DataItem, newItem: DataItem): Boolean {
return oldItem == newItem
}
}
新增並提交標頭
- 在
SleepNightAdapter
內,於onCreateViewHolder()
下方定義函式addHeaderAndSubmitList()
,如下所示。這個函式會接受SleepNight
清單。您不會使用ListAdapter
提供的submitList()
提交清單,而是使用這個函式新增標頭,然後提交清單。
fun addHeaderAndSubmitList(list: List<SleepNight>?) {}
- 在
addHeaderAndSubmitList()
內,如果傳入的清單是null
,則只會傳回標頭;否則,請將標頭附加至清單開頭,然後提交清單。
val items = when (list) {
null -> listOf(DataItem.Header)
else -> listOf(DataItem.Header) + list.map { DataItem.SleepNightItem(it) }
}
submitList(items)
- 開啟 SleepTrackerFragment.kt,並將對
submitList()
的呼叫變更為addHeaderAndSubmitList()
。
- 執行應用程式,並觀察標題在睡眠項目清單中顯示為第一個項目。
這個應用程式有兩項問題需要修正,其中一項是可見的,另一項則否。
- 標題會顯示在左上角,且不易辨識。
- 如果清單很短且只有一個標題,這點就沒什麼關係,但您不應在
addHeaderAndSubmitList()
的 UI 執行緒中操作清單。假設清單有數百個項目、多個標題,以及決定項目插入位置的邏輯。這項工作屬於協同程式。
將 addHeaderAndSubmitList()
變更為使用協同程式:
- 在
SleepNightAdapter
類別的頂層,定義具有Dispatchers.Default
的CoroutineScope
。
private val adapterScope = CoroutineScope(Dispatchers.Default)
- 在
addHeaderAndSubmitList()
中,於adapterScope
啟動協同程式,以操控清單。然後切換至Dispatchers.Main
內容,提交清單,如下列程式碼所示。
fun addHeaderAndSubmitList(list: List<SleepNight>?) {
adapterScope.launch {
val items = when (list) {
null -> listOf(DataItem.Header)
else -> listOf(DataItem.Header) + list.map { DataItem.SleepNightItem(it) }
}
withContext(Dispatchers.Main) {
submitList(items)
}
}
}
- 程式碼應會建構及執行,您不會看到任何差異。
目前,標題的寬度與格線上的其他項目相同,水平和垂直方向都佔用一個跨距。整個格線在水平方向上可容納三個跨度寬度的項目,因此標題應在水平方向上使用三個跨度。
如要修正標題寬度,您必須告知 GridLayoutManager
何時要將資料跨越所有資料欄。方法是在 GridLayoutManager
上設定 SpanSizeLookup
。這是設定物件,GridLayoutManager
會使用這個物件,判斷清單中每個項目要使用的跨度數量。
- 開啟 SleepTrackerFragment.kt。
- 在
onCreateView()
結尾附近,找出定義manager
的程式碼。
val manager = GridLayoutManager(activity, 3)
- 在
manager
下方定義manager.spanSizeLookup
,如下所示。您需要建立object
,因為setSpanSizeLookup
不會採用 lambda。如要在 Kotlin 中建立object
,請輸入object : classname
,在本例中為GridLayoutManager.SpanSizeLookup
。
manager.spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() {
}
- 您可能會收到編譯器錯誤,要求呼叫建構函式。如果有的話,請使用
Option+Enter
(Mac) 或Alt+Enter
(Windows) 開啟意圖選單,套用建構函式呼叫。
- 接著,您會在
object
上收到錯誤訊息,指出您需要覆寫方法。將游標放在object
上,按下Option+Enter
(Mac) 或Alt+Enter
(Windows) 開啟意圖選單,然後覆寫getSpanSize()
方法。
- 在
getSpanSize()
的主體中,傳回每個位置的正確跨度大小。位置 0 的跨度大小為 3,其他位置的跨度大小為 1。完成的程式碼應如下所示:
manager.spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() {
override fun getSpanSize(position: Int) = when (position) {
0 -> 3
else -> 1
}
}
- 如要改善標題的外觀,請開啟 header.xml,然後將這段程式碼新增至版面配置檔案 header.xml。
android:textColor="@color/white_text_color"
android:layout_marginStart="16dp"
android:layout_marginTop="16dp"
android:layout_marginEnd="16dp"
android:background="@color/colorAccent"
- 執行應用程式,畫面應如下方螢幕截圖所示。
恭喜!大功告成。
Android Studio 專案:RecyclerViewHeaders
- 標頭通常是跨越清單寬度的項目,可做為標題或分隔符。清單可以有一個標題來描述項目內容,也可以有多個標題來分組項目,並將項目彼此分開。
RecyclerView
可使用多個檢視畫面持有者,以容納異質項目集,例如標頭和清單項目。- 如要新增標頭,其中一種方法是修改轉接程式,檢查標頭需要顯示的索引,藉此使用不同的
ViewHolder
。Adapter
負責追蹤標頭。 - 新增標題的另一種方式是修改資料格的支援資料集 (清單),這也是您在本程式碼研究室中執行的操作。
新增頁首的主要步驟如下:
- 建立可保留標題或資料的
DataItem
,即可將清單中的資料抽象化。 - 在轉接器中,使用標題的版面配置建立檢視畫面容器。
- 更新轉接器及其方法,以便使用任何類型的
RecyclerView.ViewHolder
。 - 在
onCreateViewHolder()
中,傳回資料項目的正確檢視區塊控制代碼類型。 - 更新
SleepNightDiffCallback
以使用DataItem
類別。 - 建立
addHeaderAndSubmitList()
函式,使用協同程式將標頭新增至資料集,然後呼叫submitList()
。 - 實作
GridLayoutManager.SpanSizeLookup()
,讓標題只佔三個跨度。
Udacity 課程:
Android 開發人員說明文件:
本節列出的作業可由課程講師指派給學習本程式碼研究室的學員。講師可自由採取以下行動:
- 視需要指派作業。
- 告知學員如何繳交作業。
- 為作業評分。
講師可以視需求使用全部或部分建議內容,也可以自由指派任何其他合適的作業。
如果您是自行學習本程式碼研究室,不妨利用這些作業驗收學習成果。
回答問題
第 1 題
以下有關 ViewHolder
的敘述何者正確?
▢ 轉接程式可使用多個 ViewHolder
類別來保存標頭及不同類型的資料。
▢ 您可以將資料和標頭分別置於兩個 ViewHolder 中。
▢ RecyclerView
可支援多種類型的標頭,但資料必須一致。
▢ 新增標頭時,可將 RecyclerView
設為子類別,將標頭插入至正確的位置。
第 2 題
何時應將協同程式與 RecyclerView
搭配使用?選取所有正確的敘述。
▢ 永不。RecyclerView
是 UI 元素,不應使用協同程式。
▢ 對於可能導致 UI 變慢的長時間執行工作,請使用協同程式。
▢ 清單操作可能需要很長時間,因此您應一律使用協同程式執行。
▢ 使用協同程式和暫停函式,避免阻斷主執行緒。
第 3 題
使用多個 ViewHolder
時,下列哪項「不」是必要步驟?
▢ 在 ViewHolder
中,視需要提供多個要膨脹的版面配置檔案。
▢ 在 onCreateViewHolder()
中,傳回資料項目的正確檢視區塊控制代碼類型。
▢ 在 onBindViewHolder()
中,只有在 View Holder 是資料項目的正確 View Holder 類型時,才繫結資料。
▢ 泛化轉接器類別簽名,以接受任何 RecyclerView.ViewHolder
。
開始下一個課程:
如要查看本課程其他程式碼研究室的連結,請參閱 Android Kotlin 基礎知識程式碼研究室登陸頁面。