iT邦幫忙

第 11 屆 iT 邦幫忙鐵人賽

DAY 9
1

RecyclerView 也是一個非常特別值得獨立一篇來介紹的 ViewGroup,幾乎每種 app 都會有列表式的資料,無論是 Facebook 的 feed、Gmail 的郵件清單或是Google Play 的 app 列表都會有這種需求。

這種無窮無盡列表式的畫面如果要用一般 xml 來表示幾乎是不可能的而且也很不切實際。RecyclerView 的存在就是要解決這個問題,就字面上大家或許可以猜到我們將會有某種方式可以重複利用已經存在的 View,我們就一起來看他是怎麼實作的吧。

開始之前一樣是要加 gradle 的 dependencies。

dependencies {
    implementation 'androidx.recyclerview:recyclerview:1.1.0-beta04'
}

整個流程由以下 RecyclerViewRecyclerView.Adapter RecyclerView.ViewHolder 三個 class 合力呈現。

RecyclerView

RecyclerView 可以說是一個 container 來存放我們所有子 View 的地方,基本使用就跟一般的 ViewGroup 一樣,而且因為所有的子 View 都會動態綁定,所以在 xml 的設定也很單純,我們可以把 activity_main.xml 改成如下的程式碼:

<?xml version="1.0" encoding="utf-8"?>
<androidx.recyclerview.widget.RecyclerView
    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>

ViewHolder.ViewHolder

ViewHolder 就是我們儲存 View reference 的地方,大家也可以把它當成一個儲存 View 的 class。

class MyViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {

}

雖然看起來空空的,不過我們之後有需要還可以在這個 class 基礎上加其他的 function。

ViewHolder.Adapter

Adapter 是我們介接 data 跟 View 的地方,幾乎所有的工作都在這裡完成,當我們建立一個空的 Adapter 大概是這樣子:

class MyAdapter : RecyclerView.Adapter<MyViewHolder>() {
    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MyViewHolder {
        // 建立 ViewHolder 的地方,如果有同時支援多種 layout 的需求,
        // 可以複寫 getItemViewType function,
        // 這個 function 就可以拿到不同的 viewType 以供我們識別。
    }

    override fun getItemCount(): Int {
        // 回傳整個 Adapter 包含幾筆資料。
    }

    override fun onBindViewHolder(holder: MyViewHolder, position: Int) {
        // 因為 ViewHolder 會重複使用,
        // 我們要在這個 function 依據 position
        // 把正確的資料跟 ViewHolder 綁定在一起。
    }

}

因為 Adapter 需要綁定 View 我們先回到 app/src/main/res/layout 目錄下建立 item_myholder.xml 如下:

<?xml version="1.0" encoding="utf-8"?>
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content">

</TextView>

onCreateViewHolder 是我們建立 MyViewHolder 實體的地方,一般只需要直接回傳一個 ViewHolder 即可,系統會依據螢幕大小來呼叫我們的 function 決定建立幾個 ViewHolder

override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MyViewHolder {
    return MyViewHolder(
        LayoutInflater.from(parent.context)
            .inflate(
                R.layout.item_myholder,
                parent,
                false
            )
    )
}

我們先建立一組假資料放在 MyAdapter 如下:

private val data = (1..100).toList()

有了這組資料,getItemCountonBindViewHolder 就有資料可以綁定了,將來我們可以把資料改成從外部設值。

override fun getItemCount() = data.size

override fun onBindViewHolder(holder: MyViewHolder, position: Int) {
    holder.setData(data[position])
}

大家有發現 getItemCount 函數結構不見了嗎?這是因為在 kotlin 裡我們可以把 function 當作變數使用,甚至也可以當參數傳到其他地方,這個技術叫做 functional programming。更多資訊可以參考這裡:https://kotlinlang.org/docs/tutorials/kotlin-for-py/functional-programming.html

onBindViewHolder 主要的任務是把 ViewHolder 跟 data 做綁定,因為 ViewHolder 等同於 View,所以也可以視為把 View 跟 data 做綁定的地方,RecyclerView 會在子 View 滑出螢幕外的時候把 View 回收給其他需要使用的地方,所以當實際要呈現的畫面比目前螢幕大,不斷滑動的時候這個 function 會被呼叫很多次。

眼尖的讀者會發現我們把 onBindViewHolder 綁定資料的工作又丟回去給 ViewHolder 了,這是因為 ViewHolderAdapter 更適合做 View 的綁定與設值呦!大家可以慢慢體會。

回頭修改 MyViewHolder 如下:

class MyViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
    private val textView: TextView = itemView.findViewById(R.id.myTextView)

    fun setData(data: Int) {
        textView.text = "This is #$data Row"
    }
}

LayoutManager

最後還有個小設定,我們還需要 LayoutManager 來指定 RecyclerView 排列的方式,常用的有三種:LinearLayoutManagerGridLayoutManagerStaggeredGridLayoutManager,當然你也可以自己客製化自己的版本。

recyclerView.layoutManager = LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false)

回到我們的 MainActivity,把 onCreate 改成以下的程式碼:

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)
    val recyclerView = findViewById<RecyclerView>(R.id.recyclerView)
    recyclerView.adapter = MyAdapter()
    recyclerView.layoutManager = LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false)
}

最後就是把它 build 起來在手機上看看效果如何囉!沒意外的話畫面會長得像下圖這樣子,下一篇我們會繼續 深入探討 RecyclerView,不見不散囉~~

大家也可以試著 debug 看看我們的 MyAdapter 裡面的 onCreateViewHolder 被呼叫幾次以及 onBindViewHolder 被呼叫幾次(可以搭配滑動螢幕),非常有助於了解整個 RecyclerView 運作的機制。


上一篇
[Android 十全大補] ConstraintLayout
下一篇
[Android 十全大補] RecyclerView as a Pro
系列文
Android 十全大補30

尚未有邦友留言

立即登入留言