用 Android RecyclerView 建立有多種 ViewHolder 的列表

Photo by Tam Warner Minton on Unsplash
Photo by Tam Warner Minton on Unsplash
在開發 Android 時,一般會用 RecyclerView 來繪製列表。當列表中的每一列長得不太一樣時,我們就需要對每一種列建立一個 ViewHolder。讓我們看看要如何讓 RecyclerView 使用多種 ViewHolder。

在開發 Android 時,一般會用 RecyclerView 來繪製列表。當列表中的每一列長得不太一樣時,我們就需要對每一種列建立一個 ViewHolder。讓我們看看要如何讓 RecyclerView 使用多種 ViewHolder。

完整程式碼可以在 下載。

專案建立

建立一個新的 Empty Activity 專案。之後我們會在這 MainActivity 上,用 RecyclerView 繪製一個列表。列表裡面會有三種不同的列。

了解 RecyclerView

我們假設你已經知道如何使用 RecyclerView 來繪製單一 ViewHolder 的列表。如果你還沒使用過 RecyclerView,或是不太熟悉它的話,可以先參考以下的文章。

RecyclerView.Adapter

新增 RowData,其程式碼如下。它是所有種列的通用 interface。裡面的 method 等等會和 adapter 一起講解。

package com.waynestalk.androidrecyclerviewmultipleitemsexample

import android.view.View
import androidx.recyclerview.widget.RecyclerView

interface RowData {
    val layout: Int
    fun onCreateViewHolder(view: View): RecyclerView.ViewHolder
    fun onBindViewHolder(viewHolder: RecyclerView.ViewHolder)
}

新增 RowAdapter,其程式碼如下。

package com.waynestalk.androidrecyclerviewmultipleitemsexample

import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView

class RowAdapter(private val list: List<RowData>) :
    RecyclerView.Adapter<RecyclerView.ViewHolder>() {
    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
        val rowData = list.find { it.layout == viewType }!!
        val inflater = LayoutInflater.from(parent.context)
        val view = inflater.inflate(viewType, parent, false)
        return rowData.onCreateViewHolder(view)
    }

    override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
        val rowData = list[position]
        rowData.onBindViewHolder(holder)
    }

    override fun getItemViewType(position: Int): Int {
        return list[position].layout
    }

    override fun getItemCount(): Int {
        return list.size
    }
}

RowAdapter 繼承 RecyclerView.Adapter,並且接收一個 List<RowData>。

  • getItemCount():傳回列的總數。
  • getItemViewType():ItemViewType 指的是當前列(item view)的 type。所以說,有多少種 item view,就會有多少種 type。這邊回傳 layout,因為剛好 layout 的 type 是 Int,而且每一種 item view 都有自己的 layout。
  • onCreateViewHolder():在這 method 裡,viewType 其實就是 layout。找出使用相同 layout 的 RowData,呼叫 RowData.onCreateViewHolder(),在那裡面會建立 ViewHolder。
  • onBindViewHolder():在這 method 裡,呼叫 RowData.onBindViewHolder() 來綁定資料。

建立多種 ViewHolder

接下來,我們來建立三種 ViewHolder 來熟悉一下如何使用 RowData 來建立各種不同的 ViewHolder。

TextRowData

新增 row_text.xml。它包含兩個 TextView,一個顯示 title,一個顯示值。

<?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"
    android:layout_width="match_parent"
    android:layout_height="wrap_content">

    <TextView
        android:id="@+id/titleTextView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <TextView
        android:id="@+id/valueTextView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>

新增 TextRowData 並繼承 RowData。TextRowData 相當地簡單,所以我們就不多加解釋了。

package com.waynestalk.androidrecyclerviewmultipleitemsexample

import android.view.View
import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView

class TextRowData(private val title: String, private val value: String) : RowData {
    inner class ViewHolder(view: View) : RecyclerView.ViewHolder(view) {
        val titleTextView: TextView = itemView.findViewById(R.id.titleTextView)
        val valueTextView: TextView = itemView.findViewById(R.id.valueTextView)
    }

    override val layout = R.layout.row_text

    override fun onCreateViewHolder(view: View) = ViewHolder(view)

    override fun onBindViewHolder(viewHolder: RecyclerView.ViewHolder) {
        viewHolder as ViewHolder
        viewHolder.titleTextView.text = title
        viewHolder.valueTextView.text = value
    }
}

EditRowData

新增 row_edit.xml。它顯示一個 title,和一個文字輸入框。

<?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">

    <TextView
        android:id="@+id/titleTextView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <EditText
        android:id="@+id/valueEditText"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:importantForAutofill="no"
        android:inputType="textPersonName"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        tools:ignore="LabelFor" />

</androidx.constraintlayout.widget.ConstraintLayout>

新增 EditRowData。它和 TextRowData 差不多,但是多了一個 doAfterTextChanged 的 callback。當輸入框裡面的文字被更動後,callback 就會被呼叫。

package com.waynestalk.androidrecyclerviewmultipleitemsexample

import android.text.Editable
import android.view.View
import android.widget.EditText
import android.widget.TextView
import androidx.core.widget.doAfterTextChanged
import androidx.recyclerview.widget.RecyclerView

class EditRowData(
    private val title: String,
    private val value: String,
    private val doAfterTextChanged: (text: String) -> Unit
) : RowData {
    inner class ViewHolder(view: View) : RecyclerView.ViewHolder(view) {
        val titleTextView: TextView = itemView.findViewById(R.id.titleTextView)
        val valueEditText: EditText = itemView.findViewById(R.id.valueEditText)
    }

    override val layout = R.layout.row_edit

    override fun onCreateViewHolder(view: View) = ViewHolder(view)

    override fun onBindViewHolder(viewHolder: RecyclerView.ViewHolder) {
        viewHolder as ViewHolder
        viewHolder.titleTextView.text = title
        viewHolder.valueEditText.text = Editable.Factory.getInstance().newEditable(value)
        viewHolder.valueEditText.doAfterTextChanged { it.toString().let(doAfterTextChanged) }
    }
}

SwitchRowData

新增 row_switch.xml。它顯示一個 title,和一個開關。

<?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"
    android:layout_width="match_parent"
    android:layout_height="wrap_content">

    <TextView
        android:id="@+id/titleTextView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <androidx.appcompat.widget.SwitchCompat
        android:id="@+id/valueSwitch"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>

新增 SwitchRowData,它和 EditRowData 差不多。

package com.waynestalk.androidrecyclerviewmultipleitemsexample

import android.view.View
import android.widget.TextView
import androidx.appcompat.widget.SwitchCompat
import androidx.recyclerview.widget.RecyclerView

class SwitchRowData(
    private val title: String,
    private val value: Boolean,
    private val onCheckedChange: (Boolean) -> Unit
) : RowData {
    inner class ViewHolder(view: View) : RecyclerView.ViewHolder(view) {
        val titleTextView: TextView = itemView.findViewById(R.id.titleTextView)
        val valueSwitch: SwitchCompat = itemView.findViewById(R.id.valueSwitch)
    }

    override val layout = R.layout.row_switch

    override fun onCreateViewHolder(view: View) = ViewHolder(view)

    override fun onBindViewHolder(viewHolder: RecyclerView.ViewHolder) {
        viewHolder as ViewHolder
        viewHolder.titleTextView.text = title
        viewHolder.valueSwitch.isChecked = value
        viewHolder.valueSwitch.setOnCheckedChangeListener { buttonView, isChecked ->
            onCheckedChange(isChecked)
        }
    }
}

在 MainActivity 中使用 RowAdapter

最後,我們在 MainActivity 上用 RecyclerView 繪製一個列表。

新增 activity_main.xml,裡面顯示一個 RecyclerView。

<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/mainRecyclerView"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />

</androidx.coordinatorlayout.widget.CoordinatorLayout>

在 MainActivity 裡,新增 RowAdapter,並新增三個列。

package com.waynestalk.androidrecyclerviewmultipleitemsexample

import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        val recyclerView = findViewById<RecyclerView>(R.id.mainRecyclerView)
        recyclerView.adapter = RowAdapter(
            listOf(
                TextRowData("Name", "Wayne's Take"),
                EditRowData("Phone Number", "12345678") { println("Phone number: $it") },
                SwitchRowData("Registered", false) { println("Registered: $it") }
            )
        )
        recyclerView.layoutManager = LinearLayoutManager(this)
    }
}

這樣就大功完成了!

結論

本章中最主要的就是 RowData。它將每個列相關的邏輯與程式碼獨立出來,並都包含在一個 class 裡面。這樣不但降低了 RowAdapter 的複雜度,每個檔案的程式碼也相當地短。

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *

You May Also Like
Photo by Hans-Jurgen Mager on Unsplash
Read More

Kotlin Coroutine 教學

Kotlin 的 coroutine 是用來取代 thread。它不會阻塞 thread,而且還可以被取消。Coroutine core 會幫你管理 thread 的數量,讓你不需要自行管理,這也可以避免不小心建立過多的 thread。
Read More
Photo by Gemma Evans on Unsplash
Read More

Android:Hilt 依賴注入

Hilt 是基於 Dagger 且設計在 Android 上使用的 dependency injection library。所以在開發 Android 時,使用 Hilt 會比使用 Dagger 更加地方便。本文章將介紹如何使用 Hilt。
Read More