Android Kotlin 基礎課程 07.5:RecyclerView 中的標頭

本程式碼研究室是 Android Kotlin 基礎課程的一部分。使用程式碼研究室逐步完成程式碼課程後,您將能充分發揮本課程的潛能。所有課程程式碼研究室清單均列於 Android Kotlin 基礎程式碼研究室到達網頁

簡介

在這個程式碼研究室中,您將瞭解如何新增可在 RecyclerView 上顯示的清單寬度的標頭。您從先前的程式碼研究室提供的睡眠追蹤應用程式進行建構。

須知事項

  • 如何使用活動、片段和檢視來建構基本的使用者介面。
  • 如何在片段之間瀏覽,以及如何使用 safeArgs 在片段之間傳送資料。
  • 查看模型、查看模型工廠、轉換,以及 LiveData 及其觀測器。
  • 如何建立 Room 資料庫、建立 DAO 以及定義實體。
  • 如何使用協同程式處理資料庫互動和其他長時間執行的工作。
  • 如何實作具有 AdapterViewHolder 和項目版面配置的基本 RecyclerView
  • 如何實作 RecyclerView 的資料繫結。
  • 如何建立及使用繫結轉接程式來轉換資料。
  • 如何使用 GridLayoutManager
  • 系統如何擷取及處理 RecyclerView. 項目的點擊次數

課程內容

  • 如何將多個 ViewHolderRecyclerView 搭配使用,以新增具有不同版面配置的項目。具體來說,如何使用第二個 ViewHolderRecyclerView 中顯示的項目上方新增標頭。

執行步驟

  • 根據上一系列程式碼研究室的 TrackMySleepquality 應用程式進行建構。
  • 新增一個標題,在螢幕上方 (RecyclerView) 上方顯示橫跨螢幕寬度的螢幕寬度。

您開始的睡眠追蹤器應用程式有三個螢幕,以片段表示,如下圖所示。

第一個畫面 (如左側所示) 為開始和停止追蹤的按鈕。螢幕會顯示使用者的某些睡眠資料。[清除] 按鈕永久刪除應用程式已收集的所有資料。第二個螢幕中間部分則用於選取睡眠品質評分。第三個畫面是使用者在輕觸網格中的項目時開啟的詳細資料檢視畫面。

這個應用程式使用簡化的架構搭配 UI 控制器、檢視模型、LiveData,以及可保存睡眠資料的 Room 資料庫。

在這個程式碼研究室中,您可以在標題的格狀面板中新增標頭。您的最終主畫面看起來會像這樣:

本程式碼研究室涵蓋 RecyclerView 中涵蓋使用不同版面配置項目的一般原則。常見的情況之一,就是在清單或網格中加入標題。清單可以含有單一標題來描述項目內容。一個清單也可以包含多個標題,以便將多個項目分門別類,並分門別類。

RecyclerView」不知道任何資料,或是各個項目採用的版面配置類型。LayoutManager 會排列畫面上的項目,但轉接器會調整要顯示的資料,並將檢視持有人傳遞給 RecyclerView。因此您將加入程式碼,以便在轉接程式中建立標頭。

新增標頭的兩種方法

RecyclerView 中,清單中每個項目都會對應至從 0 開始的索引號碼。例如:

[實際資料] -> [轉接器觀看次數]

[0:SleepNight] ->> [0:SleepNight]

[1:睡眠] ->> [1:睡眠]

[2:SleepNight] ->> [2:SleepNight]

在清單中新增標頭的其中一種方法,是為索引指定不同的 ViewHolder,方法是檢查需要顯示標頭的索引。Adapter 負責追蹤標頭。舉例來說,如要在表格頂端顯示一個標頭,您在提供 0 索引的項目時,必須傳回不同的標頭 ViewHolder。然後,所有其他項目都會以標頭偏移的方式進行對應,如下所示。

[實際資料] -> [轉接器觀看次數]

[0:標題]

[0:SleepNight] ->> [1:SleepNight]

[1:睡眠] ->> [2:睡眠]

[2:SleepNight] ->>; [3:睡眠。

另一種新增標頭的方法是修改資料格線的備份資料集。由於需要顯示的所有資料都儲存在清單中,因此您可以修改清單來加入代表項目的項目。這個概念比較容易理解,但您必須思考如何設計物件,以便將不同的項目類型合併成一份清單。這樣一來,轉接器就會顯示傳送到它的項目。因此位置 0 的項目是標頭,位置 1 的項目是 SleepNight,直接對應到螢幕上的內容。

[實際資料] -> [轉接器觀看次數]

[0:標頭] -> [0:標題]

[1:睡眠] ->> [1:睡眠]

[2:SleepNight] ->> [2:SleepNight]

[3:SleepNight] ->> [3:SleepNight]

每種方法各有優缺點。變更資料集並不會對其餘的轉接程式程式碼進行大幅變更,您可以透過操縱資料清單來新增標頭邏輯。另一方面,藉由檢查標頭的索引,使用不同的 ViewHolder,可使標題的版面配置更加自由。此外,變壓器也能讓資料在未經修改資料的情況下處理資料調整至視圖。

在這個程式碼研究室中,您更新了 RecyclerView,以便在清單開頭顯示標頭。在這種情況下,您的應用程式會將標頭的 ViewHolder 用於資料項目。應用程式會檢查清單的索引,以決定要使用哪一個ViewHolder

步驟 1:建立 DataItem 類別

如要簡化項目類型,並讓轉接程式只處理「items」,請建立一個代表 SleepNightHeader 的帳戶持有人類別。資料集就會成為資料持有者項目清單。

您可以從 GitHub 取得入門應用程式,或繼續使用在上一個程式碼研究室中建立的 SleepTracker 應用程式。

  1. 從 GitHub 下載 RecyclerViewHeaders-Starter 程式碼。RecyclerViewHeaders-Starter 目錄包含此程式碼研究室所需的 SleepTracker 應用程式起始版本。您也可以繼續使用先前的程式碼研究室所完成的應用程式。
  2. 開啟 SleepNightAdapter.kt
  3. SleepNightListener 類別下方,定義一個名為 DataItemsealed 類別,代表資料項目。

    sealed 類別會定義一個封閉類型,這表示您必須在此檔案中定義 DataItem 的所有子類別。因此,編譯器已知子類別數量。您程式碼的另一部分無法定義新的 DataItem 類型,可能會中斷轉接程式。
sealed class DataItem {

 }
  1. DataItem 類別的主體中定義兩個類別,代表不同類型的資料項目。第一項是 SleepNightItem,它是 SleepNight 的包裝函式,因此會使用名為 sleepNight 的單一值。如要將它設為密封類別的一部分,請延長 DataItem
data class SleepNightItem(val sleepNight: SleepNight): DataItem()
  1. 第二個類別為 Header,用來代表標頭。由於標頭沒有任何實際資料,因此您可以將它宣告為 object。這表示只有一個 Header 例項。同樣地,也請延長 DataItem
object Header: DataItem()
  1. DataItem 內,在類別層級定義名為 idabstract Long 屬性。當轉接程式使用 DiffUtil 來判斷項目是否已變更及變更時,DiffItemCallback 必須取得每個項目的 ID。您會看到 SleepNightItemHeader,必須覆寫抽象屬性 id
abstract val id: Long
  1. SleepNightItem 中,覆寫 id 以傳回 nightId
override val id = sleepNight.nightId
  1. Header 中,覆寫 id 以傳回 Long.MIN_VALUE,這是一個非常小的數字 (在 63 的指定範圍內,也就是 -2)。因此,這個值永遠不會與任何 nightId 衝突。
override val id = Long.MIN_VALUE
  1. 您完成的程式碼應該如下所示,您的應用程式也應該在無錯誤的情況下建構。
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

  1. 在名為 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" />
  1. "Sleep Results" 擷取到字串資源中,並呼叫 header_text
<string name="header_text">Sleep Results</string>
  1. SleepNightAdapterSleepNightAdapter.kt 中,在 ViewHolder 類別上方建立新的 TextViewHolder 類別。此類別會放大 textview.xml 版面配置,並傳回 TextViewHolder 執行個體。由於你先前已完成這個步驟,因此你會看到以下程式碼,而你必須匯入 ViewR
    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 類型,還要能使用任何類型的檢視區塊。

定義項目類型

  1. SleepNightAdapter.kt 中,在頂層的 import 陳述式下方和 SleepNightAdapter 上方,為資料檢視類型定義兩個常數。

    RecyclerView 需要區分各個項目的檢視類型,才能正確指派一個畫面。
    private val ITEM_VIEW_TYPE_HEADER = 0
    private val ITEM_VIEW_TYPE_ITEM = 1
  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 定義

  1. SleepNightAdapter 的定義中,將 ListAdapter 的第一個引數從 SleepNight 更新為 DataItem
  2. SleepNightAdapter 的定義中,將 ListAdapter 的第二個一般引數從 SleepNightAdapter.ViewHolder 變更為 RecyclerView.ViewHolder。您會看到一些必要更新的錯誤,課程標題應如下所示。
class SleepNightAdapter(val clickListener: SleepNightListener):
       ListAdapter<DataItem, RecyclerView.ViewHolder>(SleepNightDiffCallback()) {

更新 onCreateViewHolder()

  1. 變更 onCreateViewHolder() 的簽名以傳回 RecyclerView.ViewHolder
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder
  1. 展開 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()

  1. onBindViewHolder() 的參數類型從 ViewHolder 變更為 RecyclerView.ViewHolder
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int)
  1. 新增條件,僅當持有者是 ViewHolder 時,才將資料指派給觀看者。
        when (holder) {
            is ViewHolder -> {...}
  1. 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 回呼

  1. 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
    }
}

新增並提交標頭

  1. SleepNightAdapteronCreateViewHolder() 下方,定義 addHeaderAndSubmitList() 函式,如下所示。這個函式會使用 SleepNight 清單。不要使用 submitList() 提供的ListAdapter 來提交您的清單,而是使用此功能新增標題,然後提交清單。
fun addHeaderAndSubmitList(list: List<SleepNight>?) {}
  1. addHeaderAndSubmitList() 中,如果傳遞清單為 null,只傳回標頭,否則請將標頭附加到清單標題,然後提交清單。
val items = when (list) {
                null -> listOf(DataItem.Header)
                else -> listOf(DataItem.Header) + list.map { DataItem.SleepNightItem(it) }
            }
submitList(items)
  1. 開啟 SleepTrackerFragment.kt 並將呼叫變更為 submitList()addHeaderAndSubmitList()
  1. 執行您的應用程式,並觀察標頭在睡眠項目清單中的第一個項目是如何顯示。

有兩項需要修正,但其中有 1 件出現。

  • 標題會顯示在左上角,不容易區分。
  • 但只含一個標頭的簡短清單並不重要,但不建議在 UI 執行緒的 addHeaderAndSubmitList() 中處理清單。假設清單中有數百個項目、多個標頭和邏輯,以決定需要插入項目的位置。此作業屬於協同程式。

變更 addHeaderAndSubmitList() 以使用協同程式:

  1. SleepNightAdapter 類別中的頂層,使用 Dispatchers.Default 定義 CoroutineScope
private val adapterScope = CoroutineScope(Dispatchers.Default)
  1. 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)
            }
        }
    }
  1. 您的程式碼應該可以建構並執行,而且不會出現差異。

目前,標題與網格中的其他項目寬度相同,水平和垂直都橫跨一個跨度。整個格狀空間可依水平調整 3 個跨度,因此標題應水平使用三個跨度。

如要修正標頭寬度,您必須告知 GridLayoutManager,何時該跨越所有資料欄的資料。方法是在 GridLayoutManager 上設定 SpanSizeLookup。這是 GridLayoutManager 的設定物件,用於決定清單中的每個項目所跨度的時距。

  1. 開啟 SleepTrackerFragment.kt
  2. 尋找您定義 manager 的程式碼,位於 onCreateView() 的結尾。
val manager = GridLayoutManager(activity, 3)
  1. manager 底下定義 manager.spanSizeLookup,如圖所示。你必須製作 object,因為 setSpanSizeLookup 不會帶到 lambda。如要在 Kotlin 中製作 object,請輸入 object : classname (在本例中為 GridLayoutManager.SpanSizeLookup)。
manager.spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() {
}
  1. 您可能會收到編譯器錯誤以呼叫建構函式。假如要這麼做,請使用 Option+Enter (Mac) 或 Alt+Enter (Windows) 開啟意圖選單,以套用建構函式呼叫。
  1. 這時您會在 object 上收到錯誤訊息,指出您必須覆寫方法。將遊標移到 object 上,按下 Option+Enter (Mac) 或 Alt+Enter (Windows) 開啟意圖選單,然後覆寫 getSpanSize() 方法。
  1. getSpanSize() 的主體中,傳回每個位置的時距大小。位置 0 的跨度大小為 3,其他位置的跨度大小為 1。您填妥的程式碼應該如下所示:
    manager.spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() {
            override fun getSpanSize(position: Int) =  when (position) {
                0 -> 3
                else -> 1
            }
        }
  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"
  1. 執行您的應用程式。應用程式看起來應該會像這樣。

恭喜!這樣就大功告成了。

Android Studio 專案:RecyclerViewHeaders

  • 「標題」通常是橫跨清單寬度的項目,而且具有標題或分隔符。清單可以用單一標題描述項目內容,或用多個標頭將項目分組,並以不同方式分隔項目。
  • RecyclerView 可使用多個檢視區塊來容納不同的項目組,例如標頭和清單項目。
  • 新增標頭的其中一種方法,就是在需要顯示標頭的索引中修改轉接程式,以便使用其他 ViewHolderAdapter 負責追蹤標頭。
  • 另一種新增標頭的方法是修改資料格線的備份資料集 (清單),也就是您在這個程式碼研究室中所做的操作。

以下是新增標頭的主要步驟:

  • 建立可存放標頭或資料的 DataItem,以抽取名單中的資料。
  • 在轉接頭中建立標題使用的版面配置。
  • 將轉接程式及其方法更新為使用任何類型的 RecyclerView.ViewHolder
  • onCreateViewHolder() 中,針對資料項目傳回正確的檢視區塊類型。
  • 如要使用 DataItem 類別,請更新 SleepNightDiffCallback
  • 建立使用協同程式的 addHeaderAndSubmitList() 函式,將標頭新增至資料集,然後呼叫 submitList()
  • 實作 GridLayoutManager.SpanSizeLookup() 可限定標頭只有三個跨度。

Udacity 課程:

Android 開發人員說明文件:

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

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

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

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

回答這些問題

第 1 題

以下關於「ViewHolder」的敘述何者正確?

▢ 轉接程式可使用多個 ViewHolder 類別來存放標頭和各種類型的資料。

▢ 只能有一位資料持有者和一個標題檢視者。

RecyclerView 支援多種標頭,但資料必須統一。

▢ 新增標頭時,只要將 RecyclerView 設為子類別,即可將標頭插入正確位置。

第 2 題

RecyclerView 何時應使用協同程式?請選取所有正確的敘述。

▢ 永不。RecyclerView 是 UI 元素,不應使用協同程式。

▢ 使用協同程式處理長時間執行的工作會拖慢使用者介面。

▢ 清單操作可能耗時過久,請一律使用協同程式進行清單處理。

▢ 使用包含懸置函式的協同程式,避免封鎖主執行緒。

第 3 題

使用多個 ViewHolder 時,「不需」執行以下哪一項動作?

▢ 請在 ViewHolder 中提供多個版面配置檔案,並視需求加入。

▢ 在 onCreateViewHolder() 中,為資料項目傳回正確的檢視區塊類型。

onBindViewHolder()

▢ 通用轉接器類別簽名,以接受任何 RecyclerView.ViewHolder

開始下一門課程:8.1 取得網際網路資料

如要瞭解本課程中其他程式碼研究室的連結,請參閱 Android Kotlin 基礎程式碼程式碼到達網頁