Android Kotlin 基礎知識 07.1:RecyclerView 基礎知識

這個程式碼研究室是 Android Kotlin 基礎知識課程的一部分。如果您按部就班完成程式碼研究室,就能充分體驗到本課程的價值。所有課程程式碼研究室都列在 Android Kotlin 基礎知識程式碼研究室到達網頁。

簡介

本程式碼研究室會說明如何使用 RecyclerView 顯示項目清單。您將以先前一系列程式碼研究室中的睡眠追蹤器應用程式為基礎,瞭解如何使用 RecyclerView 和建議的架構,以更完善且多用途的方式顯示資料。

必備知識

您必須已經熟悉下列項目:

  • 使用活動、片段和檢視區塊建構基本使用者介面 (UI)。
  • 在片段之間導覽,以及使用 safeArgs 在片段之間傳遞資料。
  • 使用檢視區塊模型、檢視區塊模型工廠、轉換和 LiveData,以及這些項目的觀察器。
  • 建立 Room 資料庫、建立 DAO,以及定義實體。
  • 使用協同程式處理資料庫工作和其他長時間執行的工作。

課程內容

  • 如何搭配使用 RecyclerView、Adapter 和 ViewHolder 顯示項目清單。

學習內容

  • 將上一堂課的 TrackMySleepQuality 應用程式改為使用 RecyclerView 顯示睡眠品質資料。

在本程式碼研究室中,您將建構應用程式的 RecyclerView 部分,追蹤睡眠品質。應用程式會使用 Room 資料庫儲存一段時間內的睡眠資料。

如圖所示,入門版睡眠追蹤應用程式有兩個畫面,分別以片段表示。

如左側所示,第一個畫面會顯示開始和停止追蹤的按鈕。這個畫面也會顯示使用者的所有睡眠資料。「清除」按鈕會永久刪除應用程式為使用者收集的所有資料。右側的第二個畫面用於選取睡眠品質評分。

這個應用程式採用簡化架構,包含 UI 控制器、ViewModel 和 LiveData。應用程式也會使用 Room 資料庫,讓睡眠資料保持不變。

第一個畫面顯示的睡眠夜數清單可以正常運作,但並不美觀。應用程式會使用複雜的格式化工具,為文字檢視區塊建立文字字串,並為品質建立數字。此外,這項設計無法擴充。在本程式碼研究室中修正所有問題後,最終應用程式的功能會相同,主畫面如下所示:

顯示資料清單或格線是 Android 中最常見的 UI 工作之一。清單的複雜程度不一,從簡單到非常複雜都有。文字檢視畫面可能會顯示簡單的資料,例如購物清單。複雜清單 (例如附註的度假目的地清單) 可能會在含有標題的捲動格線中,向使用者顯示許多詳細資料。

為支援所有這些用途,Android 提供 RecyclerView 小工具。

RecyclerView 的最大優點是可有效處理大型清單:

  • 根據預設,RecyclerView 只會處理或繪製目前畫面上的可見項目。舉例來說,如果您的清單有 1,000 個元素,但只有 10 個顯示,則 RecyclerView 只會將 10 個項目繪製於畫面上。使用者捲動畫面時,RecyclerView 會判斷螢幕上應該顯示的新項目,然後做好足以顯示這些項目的工作。
  • 當項目捲動至螢幕外時,系統會回收該項目的檢視畫面。這表示捲動至螢幕上的新內容會填滿該項目。這種 RecyclerView 行為不但可省下大量處理時間,還能讓清單更順暢的捲動。
  • 項目變更時,RecyclerView 可以更新該項目,不必重新繪製整個清單。顯示複雜項目清單時,這項功能可大幅提升效率!

在下方的序列中,您可以看到一個已填入資料 ABC 的檢視畫面。在該畫面捲動至螢幕外之後,RecyclerView 會讓新資料 (XYZ) 重複使用檢視畫面。

轉接器模式

如果你經常往返使用不同插座的國家/地區,可能知道如何使用轉接器將裝置插入插座。轉接器可將一種插頭轉換成另一種,也就是將一個介面轉換成另一個介面。

軟體工程中的轉接器模式可協助物件與其他 API 搭配運作。RecyclerView 會使用轉接程式將應用程式資料轉換為 RecyclerView 可顯示的內容,而不會變更應用程式儲存及處理資料的方式。以睡眠追蹤應用程式為例,您會建構轉接器,將 Room 資料庫中的資料調整為 RecyclerView 可顯示的格式,但不會變更 ViewModel。

實作 RecyclerView

如要在 RecyclerView 中顯示資料,您需要下列部分:

  • 要顯示的資料。
  • 版面配置檔案中定義的 RecyclerView 執行個體,做為檢視區塊的容器。
  • 單一資料項目的版面配置。
    如果所有清單項目看起來都一樣,你可以為所有項目使用相同的版面配置,但這並非必要。項目版面配置必須與片段的版面配置分開建立,這樣才能一次建立一個項目檢視畫面並填入資料。
  • 版面配置管理工具。
    版面配置管理工具會處理檢視區塊中 UI 元件的組織 (版面配置)。
  • 檢視區塊容器。
    檢視區塊容器會擴充 ViewHolder 類別。其中包含用於顯示項目版面配置中某個項目的檢視區塊資訊。檢視畫面保留項也會新增 RecyclerView 用來有效在螢幕上移動檢視畫面的資訊。
  • 轉接器。
    轉接器會將資料連結至 RecyclerView。它會調整資料,以便顯示在 ViewHolder 中。RecyclerView 會使用轉接介面來判斷如何在畫面上顯示資料。

在這項工作中,您要在版面配置檔案中新增 RecyclerView,並設定 Adapter,將睡眠資料公開給 RecyclerView。

步驟 1:新增 RecyclerView 和 LayoutManager

在這個步驟中,您要在 fragment_sleep_tracker.xml 檔案中將 ScrollView 替換為 RecyclerView。

  1. 從 GitHub 下載 RecyclerViewFundamentals-Starter 應用程式。
  2. 建構並執行應用程式。請注意,資料會以簡單文字的形式顯示。
  3. 在 Android Studio 的「Design」分頁中,開啟 fragment_sleep_tracker.xml 版面配置檔案。
  4. 在「Component Tree」窗格中,刪除 ScrollView。這項操作也會刪除 ScrollView 內的 TextView。
  5. 在「Palette」窗格中,捲動左側的元件類型清單,找出並選取「Containers」。
  6. 將 RecyclerView 從「Palette」窗格拖曳至「Component Tree」窗格。將 RecyclerView 放在 ConstraintLayout 內。

  1. 如果系統開啟對話方塊,詢問是否要新增依附元件,請按一下「OK」,讓 Android Studio 將 recyclerview 依附元件新增至 Gradle 檔案。系統可能需要幾秒鐘,然後應用程式就會開始同步。

  1. 開啟模組 build.gradle 檔案,捲動至結尾,並記下新的依附元件,程式碼如下所示:
implementation 'androidx.recyclerview:recyclerview:1.0.0'
  1. 切換回「fragment_sleep_tracker.xml」。
  2. 在「Text」(文字) 分頁中,尋找下方程式碼:RecyclerView
<androidx.recyclerview.widget.RecyclerView
   android:layout_width="match_parent"
   android:layout_height="match_parent" />
  1. 將 RecyclerView 指派給 sleep_list 的 id。
android:id="@+id/sleep_list"
  1. 將 RecyclerView 放置在 ConstraintLayout 內,填滿畫面其餘部分。如要這麼做,請將 RecyclerView 的頂端限制為「開始」按鈕,底部限制為「清除」按鈕,兩側則限制為父項。在版面配置編輯器或 XML 中,使用下列程式碼將版面配置寬度和高度設為 0 dp:
    android:layout_width="0dp"
    android:layout_height="0dp"
    app:layout_constraintBottom_toTopOf="@+id/clear_button"
    app:layout_constraintEnd_toEndOf="parent"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintTop_toBottomOf="@+id/stop_button"
  1. 在 RecyclerView XML 中加入版面配置管理工具。每個 RecyclerView 都需要版面配置管理工具,告知如何在清單中放置項目。Android 提供 LinearLayoutManager,預設會將項目排成全寬列的垂直清單。
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
  1. 切換至「Design」(設計) 分頁,您會發現新增的限制條件已導致 RecyclerView 擴展,填滿可用空間。

步驟 2:建立清單項目版面配置和文字檢視區塊持有者

RecyclerView 只是容器,在這個步驟中,您會建立版面配置和基礎架構,以便在 RecyclerView 中顯示項目。

為了盡快取得可用的 RecyclerView,您一開始會使用簡單的清單項目,只顯示睡眠品質的數值。為此,您需要檢視區塊容器 TextItemViewHolder。您也需要資料的檢視畫面 (TextView)。(在後續步驟中,您會進一步瞭解檢視區塊持有者,以及如何配置所有睡眠資料)。

  1. 建立名為 text_item_view.xml 的版面配置檔案。您使用哪個做為根元素並不重要,因為您會取代範本程式碼。
  2. 在 text_item_view.xml 中,刪除所有提供的程式碼。
  3. 在開頭和結尾新增 TextView,並將邊框間距設為 16dp,文字大小設為 24sp。讓寬度與父項相符,高度則包住內容。由於這個檢視區塊顯示在 RecyclerView 內,因此您不必將檢視區塊放在 ViewGroup 內。
<?xml version="1.0" encoding="utf-8"?>
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
    android:textSize="24sp"
    android:paddingStart="16dp"
    android:paddingEnd="16dp"
    android:layout_width="match_parent"       
    android:layout_height="wrap_content" />
  1. 開啟 Util.kt。捲動至結尾,然後新增下方顯示的定義,建立 TextItemViewHolder 類別。將程式碼放在檔案底部,也就是最後一個右大括號之後。程式碼會放在 Util.kt 中,因為這個檢視區塊持有者是暫時的,稍後會替換。
class TextItemViewHolder(val textView: TextView): RecyclerView.ViewHolder(textView)
  1. 如果系統提示,請匯入 android.widget.TextView 和 androidx.recyclerview.widget.RecyclerView。

步驟 3:建立 SleepNightAdapter

實作 RecyclerView 的核心工作是建立轉接器。您有商品檢視畫面的簡單檢視區塊容器,以及每個商品的版面配置。您現在可以建立轉接器。轉接程式會建立檢視區塊容器,並填入要顯示在 RecyclerView 中的資料。

  1. 在 sleeptracker 套件中,建立名為 SleepNightAdapter 的新 Kotlin 類別。
  2. 讓 SleepNightAdapter 類別擴充 RecyclerView.Adapter。這個類別稱為 SleepNightAdapter,因為它會將 SleepNight 物件調整為 RecyclerView 可用的內容。轉接器需要知道要使用哪個檢視區塊持有者,因此請傳入 TextItemViewHolder。系統提示時,請匯入必要元件,然後您會看到錯誤訊息,因為必須實作強制方法。
class SleepNightAdapter: RecyclerView.Adapter<TextItemViewHolder>() {}
  1. 在 SleepNightAdapter 的頂層,建立 listOf SleepNight 變數來存放資料。
var data =  listOf<SleepNight>()
  1. 在 SleepNightAdapter 中,覆寫 getItemCount(),傳回 data 中睡眠夜數清單的大小。RecyclerView 需要知道轉接程式有多少項目可供顯示,因此會呼叫 getItemCount()。
override fun getItemCount() = data.size
  1. 在 SleepNightAdapter 中,覆寫 onBindViewHolder() 函式,如下所示。

    RecyclerView 會呼叫 onBindViewHolder() 函式,顯示指定位置的清單項目資料。因此 onBindViewHolder() 方法會採用兩個引數:檢視區塊持有者,以及要繫結的資料位置。以這個應用程式為例,持有者是 TextItemViewHolder,位置則是清單中的位置。
override fun onBindViewHolder(holder: TextItemViewHolder, position: Int) {
}
  1. 在 onBindViewHolder() 中,為資料中指定位置的項目建立變數。
 val item = data[position]
  1. 您建立的 ViewHolder 具有名為 textView 的屬性。在 onBindViewHolder() 中,將 textView 的 text 設為睡眠品質分數。這段程式碼只會顯示數字清單,但這個簡單的範例可讓您瞭解轉接程式如何將資料放入檢視畫面保留項,並顯示在畫面上。
holder.textView.text = item.sleepQuality.toString()
  1. 在 SleepNightAdapter 中,覆寫並實作 onCreateViewHolder(),當 RecyclerView 需要檢視畫面持有者來表示項目時,系統就會呼叫此函式。

    這個函式會採用兩個參數,並傳回 ViewHolder。parent 參數是保存檢視區塊控制碼的檢視區塊群組,一律為 RecyclerView。如果同一個 RecyclerView 中有多個檢視區塊,就會使用 viewType 參數。舉例來說,如果您將文字檢視區塊清單、圖片和影片都放在同一個 RecyclerView 中,onCreateViewHolder() 函式就需要知道要使用哪種檢視區塊。
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): TextItemViewHolder {
}
  1. 在 onCreateViewHolder() 中,建立 LayoutInflater 的執行個體。

    版面配置加載程式知道如何從 XML 版面配置建立檢視區塊。context 說明如何正確擴充檢視區塊。在回收器檢視區塊的轉接程式中,您一律會傳入 parent 檢視區塊群組的內容,也就是 RecyclerView。
val layoutInflater = LayoutInflater.from(parent.context)
  1. 在 onCreateViewHolder() 中,要求 layoutinflater 擴充 view,藉此建立 view。

    傳入檢視區塊的 XML 版面配置,以及檢視區塊的 parent 檢視區塊群組。第三個布林值引數是 attachToRoot。這個引數必須是 false,因為 RecyclerView 在適當時會為您新增此項目到檢視區塊階層。
val view = layoutInflater
       .inflate(R.layout.text_item_view, parent, false) as TextView
  1. 在 onCreateViewHolder() 中,傳回以 view 建立的 TextItemViewHolder。
return TextItemViewHolder(view)
  1. 轉接器需要讓 RecyclerView 知道 data 何時變更,因為 RecyclerView 對資料一無所知。它只會知道介面卡提供給它的檢視區塊持有者。

    如要告知 RecyclerView 顯示的資料何時變更,請在 SleepNightAdapter 類別頂端的 data 變數中新增自訂設定器。在設定器中,為 data 提供新值,然後呼叫 notifyDataSetChanged(),觸發使用新資料重新繪製清單。
var data =  listOf<SleepNight>()
   set(value) {
       field = value
       notifyDataSetChanged()
   }

步驟 4:向 RecyclerView 說明 Adapter

RecyclerView 需要知道要使用哪個轉接器來取得檢視區塊持有者。

  1. 開啟 SleepTrackerFragment.kt。
  2. 在 onCreateview() 中建立轉接器。請將這段程式碼放在建立 ViewModel 模型之後,以及 return 陳述式之前。
val adapter = SleepNightAdapter()
  1. 將 adapter 與 RecyclerView 建立關聯。
binding.sleepList.adapter = adapter
  1. 清除並重建專案,更新 binding 物件。

    如果 binding.sleepList 或 binding.FragmentSleepTrackerBinding 附近仍顯示錯誤,請使快取失效並重新啟動。(選取「File」>「Invalidate Caches / Restart」)。

    現在執行應用程式不會發生錯誤,但輕觸「Start」,然後輕觸「Stop」時,不會看到任何顯示的資料。

步驟 5:將資料匯入轉接器

目前您已擁有轉接器,以及將資料從轉接器傳輸至 RecyclerView 的方法。現在您需要從 ViewModel 取得資料,並傳送至轉接器。

  1. 開啟 SleepTrackerViewModel。
  2. 找出 nights 變數,這個變數會儲存所有睡眠夜數,也就是要顯示的資料。透過在資料庫上呼叫 getAllNights(),即可設定 nights 變數。
  3. 從 nights 中移除 private,因為您要建立需要存取這個變數的觀察器。您的宣告應如下所示:
val nights = database.getAllNights()
  1. 在 database 套件中,開啟 SleepDatabaseDao。
  2. 找出 getAllNights() 函式。請注意,這個函式會以 LiveData 形式傳回 SleepNight 值清單。這表示 nights 變數包含 Room 持續更新的 LiveData,您可以觀察 nights,瞭解何時發生變更。
  3. 開啟 SleepTrackerFragment。
  4. 在 onCreateView() 中,於 adapter 建立完成後,在 nights 變數上建立觀察程式。

    將片段的 viewLifecycleOwner 設為生命週期擁有者,即可確保這個觀察器只會在 RecyclerView 顯示在畫面上時處於有效狀態。
sleepTrackerViewModel.nights.observe(viewLifecycleOwner, Observer {
   })
  1. 在觀察器中,每當您取得非空值 (適用於 nights) 時,請將該值指派給轉接程式的 data。以下是觀察器和設定資料的完整程式碼:
sleepTrackerViewModel.nights.observe(viewLifecycleOwner, Observer {
   it?.let {
       adapter.data = it
   }
})
  1. 建構並執行程式碼。

    如果轉接器正常運作,畫面上會顯示睡眠品質數據清單。左側的螢幕截圖顯示輕觸「開始」後出現 -1。右側的螢幕截圖顯示輕觸「停止」並選取睡眠品質評分後,更新的睡眠品質分數。

步驟 6:瞭解檢視區塊保留項目如何回收

RecyclerView 會回收檢視區塊控制代碼,也就是重複使用這些控制代碼。當檢視區塊捲動至螢幕外時,RecyclerView 會重複使用該檢視區塊,用於即將捲動至螢幕上的檢視區塊。

由於這些檢視區塊容器會重複使用,請務必確保 onBindViewHolder() 會設定或重設先前項目可能在檢視區塊容器上設定的任何自訂項目。

舉例來說,如果檢視區塊容器的睡眠品質評分低於或等於 1 分,代表睡眠品質不佳,您就可以將文字顏色設為紅色。

  1. 在 SleepNightAdapter 類別中,於 onBindViewHolder() 結尾新增下列程式碼。
if (item.sleepQuality <= 1) {
   holder.textView.setTextColor(Color.RED) // red
}
  1. 執行應用程式。
  2. 如果新增一些睡眠品質不佳的資料,數字就會變成紅色。
  3. 持續新增睡眠品質的高評分,直到畫面上出現紅色高分。

    由於 RecyclerView 會重複使用檢視區塊持有者,因此最終會重複使用其中一個紅色檢視區塊持有者,代表評分很高。高評分錯誤地顯示為紅色,

  1. 如要修正這個問題,請新增 else 陳述式,在品質小於或等於 1 時將顏色設為黑色。

    明確指出這兩個條件後,檢視區塊持有者就會為每個項目使用正確的文字顏色。
if (item.sleepQuality <= 1) {
   holder.textView.setTextColor(Color.RED) // red
} else {
   // reset
   holder.textView.setTextColor(Color.BLACK) // black
}
  1. 執行應用程式,數字應一律顯示正確顏色。

恭喜!現在您已擁有功能完整的基礎 RecyclerView。

在這項工作中,您要取代簡單的檢視區塊持有者,改用可顯示更多睡眠資料的檢視區塊持有者。

您在 Util.kt 中新增的簡單 ViewHolder 只會在 TextItemViewHolder 中包裝 TextView。

class TextItemViewHolder(val textView: TextView): RecyclerView.ViewHolder(textView)

那麼,為什麼 RecyclerView 不直接使用 TextView 呢?這行程式碼提供許多功能。ViewHolder 說明項目檢視畫面,以及項目在 RecyclerView 中的位置相關中繼資料。RecyclerView 會依賴這項功能,在清單捲動時正確放置檢視區塊,並執行有趣的操作,例如在 Adapter 中新增或移除項目時,為檢視區塊加入動畫效果。

如果 RecyclerView 需要存取 ViewHolder 中儲存的檢視區塊,可以使用檢視區塊持有人的 itemView 屬性。RecyclerView 會在將項目繫結至螢幕上顯示時、在檢視區塊周圍繪製裝飾 (例如邊框) 時,以及實作無障礙功能時使用 itemView。

步驟 1:建立項目版面配置

在這個步驟中,您要建立一個項目的版面配置檔案。版面配置包含 ConstraintLayout,以及睡眠品質的 ImageView、睡眠時長的 TextView,以及以文字顯示的睡眠品質 TextView。由於您先前已完成版面配置,請複製並貼上提供的 XML 程式碼。

  1. 建立新的版面配置資源檔案,並命名為 list_item_sleep_night。
  2. 將檔案中的所有程式碼替換成下方程式碼。然後熟悉剛才建立的版面配置。
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
   xmlns:app="http://schemas.android.com/apk/res-auto"
   xmlns:tools="http://schemas.android.com/tools"
   android:layout_width="match_parent"
   android:layout_height="wrap_content">

   <ImageView
       android:id="@+id/quality_image"
       android:layout_width="@dimen/icon_size"
       android:layout_height="60dp"
       android:layout_marginStart="16dp"
       android:layout_marginTop="8dp"
       android:layout_marginBottom="8dp"
       app:layout_constraintBottom_toBottomOf="parent"
       app:layout_constraintStart_toStartOf="parent"
       app:layout_constraintTop_toTopOf="parent"
       tools:srcCompat="@drawable/ic_sleep_5" />

   <TextView
       android:id="@+id/sleep_length"
       android:layout_width="0dp"
       android:layout_height="20dp"
       android:layout_marginStart="8dp"
       android:layout_marginTop="8dp"
       android:layout_marginEnd="16dp"
       app:layout_constraintEnd_toEndOf="parent"
       app:layout_constraintStart_toEndOf="@+id/quality_image"
       app:layout_constraintTop_toTopOf="@+id/quality_image"
       tools:text="Wednesday" />

   <TextView
       android:id="@+id/quality_string"
       android:layout_width="0dp"
       android:layout_height="20dp"
       android:layout_marginTop="8dp"
       app:layout_constraintEnd_toEndOf="@+id/sleep_length"
       app:layout_constraintHorizontal_bias="0.0"
       app:layout_constraintStart_toStartOf="@+id/sleep_length"
       app:layout_constraintTop_toBottomOf="@+id/sleep_length"
       tools:text="Excellent!!!" />
</androidx.constraintlayout.widget.ConstraintLayout>
  1. 切換至 Android Studio 中的「設計」分頁標籤。在設計檢視畫面中,版面配置看起來會像下方左側的螢幕截圖。在藍圖檢視畫面中,看起來會像右側的螢幕截圖。

步驟 2:建立 ViewHolder

  1. 開啟 SleepNightAdapter.kt。
  2. 在 SleepNightAdapter 中建立名為 ViewHolder 的類別,並擴充 RecyclerView.ViewHolder。
class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView){}
  1. 在 ViewHolder 中,取得檢視區塊的參照。您需要這個 ViewHolder 將更新的檢視區塊參照。每次繫結這個 ViewHolder 時,您都需要存取圖片和兩個文字檢視區塊。(您稍後會轉換這段程式碼,改用資料繫結)。
val sleepLength: TextView = itemView.findViewById(R.id.sleep_length)
val quality: TextView = itemView.findViewById(R.id.quality_string)
val qualityImage: ImageView = itemView.findViewById(R.id.quality_image)

步驟 3:在 SleepNightAdapter 中使用 ViewHolder

  1. 在 SleepNightAdapter 定義中,請使用您剛建立的 SleepNightAdapter.ViewHolder,而非 TextItemViewHolder。
class SleepNightAdapter: RecyclerView.Adapter<SleepNightAdapter.ViewHolder>() {

更新 onCreateViewHolder():

  1. 變更 onCreateViewHolder() 的簽名,以回傳 ViewHolder。
  2. 將版面配置膨脹器變更為使用正確的版面配置資源 list_item_sleep_night。
  3. 移除投放到 TextView 的內容。
  4. 請傳回 ViewHolder,而非 TextItemViewHolder。

    更新後的 onCreateViewHolder() 函式如下:
    override fun onCreateViewHolder(
            parent: ViewGroup, viewType: Int): ViewHolder {
        val layoutInflater = 
            LayoutInflater.from(parent.context)
        val view = layoutInflater
                .inflate(R.layout.list_item_sleep_night, 
                         parent, false)
        return ViewHolder(view)
    }

更新 onBindViewHolder():

  1. 變更 onBindViewHolder() 的簽章,讓 holder 參數成為 ViewHolder,而非 TextItemViewHolder。
  2. 在 onBindViewHolder() 中,刪除所有程式碼,但 item 的定義除外。
  3. 定義 val res,其中會保留這個檢視區塊的 resources 參照。
val res = holder.itemView.context.resources
  1. 將 sleepLength 文字檢視區塊的文字設為時間長度。複製下列程式碼,呼叫範例程式碼提供的格式化函式。
holder.sleepLength.text = convertDurationToFormatted(item.startTimeMilli, item.endTimeMilli, res)
  1. 這樣會發生錯誤,因為需要定義 convertDurationToFormatted()。開啟 Util.kt,並取消註解相關程式碼和匯入項目。(選取「程式碼」>「以行註解加入註解」)。
  2. 返回 onBindViewHolder(),使用 convertNumericQualityToString() 設定品質。
holder.quality.text= convertNumericQualityToString(item.sleepQuality, res)
  1. 您可能需要手動匯入這些函式。
import com.example.android.trackmysleepquality.convertDurationToFormatted
import com.example.android.trackmysleepquality.convertNumericQualityToString
  1. 為品質設定正確的圖示。範例程式碼中已提供新的 ic_sleep_active 圖示。
holder.qualityImage.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
})
  1. 以下是更新後的 onBindViewHolder() 函式,會為 ViewHolder 設定所有資料:
   override fun onBindViewHolder(holder: ViewHolder, position: Int) {
        val item = data[position]
        val res = holder.itemView.context.resources
        holder.sleepLength.text = convertDurationToFormatted(item.startTimeMilli, item.endTimeMilli, res)
        holder.quality.text= convertNumericQualityToString(item.sleepQuality, res)
        holder.qualityImage.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
        })
    }
  1. 執行應用程式。畫面應如下方螢幕截圖所示,顯示睡眠品質圖示,以及睡眠時間長度和睡眠品質的文字。

您的RecyclerView已完成!您已瞭解如何實作 Adapter 和 ViewHolder,並將兩者結合,透過 RecyclerView Adapter 顯示清單。

您目前的程式碼顯示了建立轉接器和檢視區塊容器的程序。不過,您可以改善這段程式碼。顯示檢視畫面保留項的程式碼和管理檢視畫面保留項的程式碼混在一起,而 onBindViewHolder() 知道如何更新 ViewHolder 的詳細資料。

在實際工作環境的應用程式中,您可能會有多個檢視區塊持有者、更複雜的介面卡,以及多位開發人員進行變更。您應建構程式碼,確保與檢視容器相關的所有內容都只存在於檢視容器中。

步驟 1:重構 onBindViewHolder()

在這個步驟中,您會重構程式碼,並將所有檢視區塊持有者功能移至 ViewHolder。重構的目的不是改變應用程式對使用者的外觀,而是讓開發人員更輕鬆安全地處理程式碼。幸好 Android Studio 提供相關工具。

  1. 在 SleepNightAdapter 的 onBindViewHolder() 中,選取所有項目,但宣告變數 item 的陳述式除外。
  2. 按一下滑鼠右鍵,然後依序選取「Refactor」>「Extract」>「Function」。
  3. 將函式命名為 bind,並接受建議的參數。按一下「確定」。

    bind() 函式會放在 onBindViewHolder() 下方。
    private fun bind(holder: ViewHolder, item: SleepNight) {
        val res = holder.itemView.context.resources
        holder.sleepLength.text = convertDurationToFormatted(item.startTimeMilli, item.endTimeMilli, res)
        holder.quality.text = convertNumericQualityToString(item.sleepQuality, res)
        holder.qualityImage.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
        })
    }
  1. 將游標放在 bind() 的 holder 參數的 holder 字詞上。按下 Alt+Enter 鍵 (在 Mac 上為 Option+Enter 鍵) 開啟意圖選單。選取「Convert parameter to receiver」(將參數轉換為接收器),將其轉換為具有下列簽章的擴充函式:
private fun ViewHolder.bind(item: SleepNight) {...}
  1. 將 bind() 函式剪下並貼到 ViewHolder。
  2. 將 bind() 設為公開。
  3. 視需要將 bind() 匯入轉接器。
  4. 由於現在已加入 ViewHolder,您可以移除簽名檔中的 ViewHolder 部分。以下是 ViewHolder 類別中 bind() 函式的最終程式碼。
fun bind(item: SleepNight) {
   val res = itemView.context.resources
   sleepLength.text = convertDurationToFormatted(
           item.startTimeMilli, item.endTimeMilli, res)
   quality.text = convertNumericQualityToString(
           item.sleepQuality, res)
   qualityImage.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:重構 onCreateViewHolder

轉接器中的 onCreateViewHolder() 方法目前會從 ViewHolder 的版面配置資源加載檢視區塊。不過,通膨與轉接器無關,而是與ViewHolder有關。通膨應發生在 ViewHolder。

  1. 在 onCreateViewHolder() 中,選取函式主體中的所有程式碼。
  2. 按一下滑鼠右鍵,然後依序選取「Refactor」>「Extract」>「Function」。
  3. 將函式命名為 from,並接受建議的參數。按一下「確定」。
  4. 將游標放在函式名稱 from 上。按下 Alt+Enter 鍵 (在 Mac 上為 Option+Enter 鍵) 開啟意圖選單。
  5. 選取「移至隨附物件」。from() 函式必須位於伴隨物件中,才能在 ViewHolder 類別上呼叫,而非在 ViewHolder 執行個體上呼叫。
  6. 將 companion 物件移至 ViewHolder 類別中。
  7. 將 from() 設為公開。
  8. 在 onCreateViewHolder() 中,將 return 陳述式變更為傳回 ViewHolder 類別中呼叫 from() 的結果。

    完成的 onCreateViewHolder() 和 from() 方法應如下列程式碼所示,且程式碼應能建構及執行,不會發生錯誤。
    override fun onCreateViewHolder(parent: ViewGroup, viewType: 
Int): ViewHolder {
        return ViewHolder.from(parent)
    }
companion object {
   fun from(parent: ViewGroup): ViewHolder {
       val layoutInflater = LayoutInflater.from(parent.context)
       val view = layoutInflater
               .inflate(R.layout.list_item_sleep_night, parent, false)
       return ViewHolder(view)
   }
}
  1. 變更 ViewHolder 類別的簽名,讓建構函式成為私有函式。由於 from() 現在是會傳回新 ViewHolder 執行個體的方法,因此不再需要呼叫 ViewHolder 的建構函式。
class ViewHolder private constructor(itemView: View) : RecyclerView.ViewHolder(itemView){
  1. 執行應用程式。應用程式應會照常建構及執行,這就是重構後預期的結果。

Android Studio 專案:RecyclerViewFundamentals

  • 顯示資料清單或格線是 Android 中最常見的 UI 工作之一。RecyclerView 經過精心設計,即使顯示極大的清單,也能有效率地運作。
  • RecyclerView 只會處理或繪製目前畫面上的可見項目。
  • 當項目捲動至螢幕外時,系統會回收其檢視畫面。這表示捲動至螢幕上的新內容會填滿該項目。
  • 軟體工程中的轉接器模式可協助物件與其他 API 搭配運作。RecyclerView 會使用轉接器將應用程式資料轉換為可顯示的內容,不必變更應用程式儲存及處理資料的方式。

如要在 RecyclerView 中顯示資料,您需要下列部分:

  • RecyclerView
    如要建立 RecyclerView 的例項,請在版面配置檔案中定義 <RecyclerView> 元素。
  • LayoutManager
    RecyclerView 會使用 LayoutManager 整理 RecyclerView 中的項目版面配置,例如以格線或線性清單的形式排列項目。

    在版面配置檔案的 <RecyclerView> 中,將 app:layoutManager 屬性設為版面配置管理工具 (例如 LinearLayoutManager 或 GridLayoutManager)。

    您也可以透過程式設定 RecyclerView 的 LayoutManager。(我們會在後續的程式碼研究室中介紹這項技術)。
  • 每個項目的版面配置
    :在 XML 版面配置檔案中,為一個資料項目建立版面配置。
  • Adapter
    :建立轉接器,準備資料並決定資料在 ViewHolder 中的顯示方式。將轉接程式與 RecyclerView 建立關聯。

    執行 RecyclerView 時,系統會使用轉接器判斷如何在畫面上顯示資料。

    轉接器需要您實作下列方法:
    – getItemCount():傳回項目數量。
    – onCreateViewHolder():傳回清單中項目的 ViewHolder。
    – onBindViewHolder():將資料調整為清單中項目的檢視畫面。

  • ViewHolder
    ViewHolder 包含檢視畫面資訊,用於顯示項目版面配置中的一個項目。
  • 轉接程式中的 onBindViewHolder() 方法會調整檢視畫面的資料。您一律會覆寫這個方法。一般來說,onBindViewHolder() 會擴充項目的版面配置,並將資料放入版面配置中的檢視區塊。
  • 由於 RecyclerView 不瞭解資料,因此 Adapter 必須在資料變更時通知 RecyclerView。使用 notifyDataSetChanged() 通知 Adapter 資料已變更。

Udacity 課程:

Android 開發人員說明文件:

本節列出的作業可由課程講師指派給學習本程式碼研究室的學員。講師可自由採取以下行動:

  • 視需要指派作業。
  • 告知學員如何繳交作業。
  • 為作業評分。

講師可以視需求使用全部或部分建議內容,也可以自由指派任何其他合適的作業。

如果您是自行學習本程式碼研究室,不妨利用這些作業驗收學習成果。

回答問題

第 1 題

RecyclerView 如何顯示項目?請選取所有適用選項。

▢ 以清單或格狀排列方式顯示項目。

▢ 垂直或水平捲動。

▢ 在平板電腦等大型裝置上,沿對角線方向捲動。

▢ 當清單或格狀排列方式不足以表現用途時,允許自訂版面配置。

第 2 題

使用 RecyclerView 的優點有哪些?請選取所有適用選項。

▢ 有效率地顯示大型清單。

▢ 自動更新資料。

▢ 盡量減少在清單中更新、刪除或新增項目時重新整理的需求。

▢ 重複使用捲動至螢幕外的檢視畫面,顯示捲動至螢幕上的下一個項目。

第 3 題

使用轉接器是基於哪些原因?請選取所有適用選項。

▢ 關注點分離原則可以簡化程式碼的變更與測試程序。

▢ 無法確定 RecyclerView 對目前所顯示的資料有什麼影響。

▢ 資料處理層不需考慮資料的顯示方式。

▢ 應用程式執行速度更快。

第 4 題

以下有關 ViewHolder 的敘述何者正確?請選取所有適用選項。

▢ ViewHolder 版面配置是在 XML 版面配置檔案中定義。

▢ 資料集中的每個資料單元都有一個 ViewHolder。

▢ 一個 RecyclerView 可以包含多個 ViewHolder。

▢ Adapter 會將資料繫結至 ViewHolder。

開始下一個課程:7.2:使用 RecyclerView 進行 DiffUtil 和資料繫結