在開發 Android 時,一般會用 RecyclerView 來繪製列表。當列表中的每一列長得不太一樣時,我們就需要對每一種列建立一個 ViewHolder。讓我們看看要如何讓 RecyclerView 使用多種 ViewHolder。
Table of Contents
專案建立
建立一個新的 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 的複雜度,每個檔案的程式碼也相當地短。









