RecyclerView封裝-結合ViewBinding 3行代碼創建Adapter!

前言

RecyclerView在項目中基本都是必備的了,
然而我們正常寫一個列表卻需要實現Adapter的onCreateViewHolder,onBindViewHolder,getItemCount,以及需要ViewHolder的眾多findViewById

這使得我們使用的成本大大增加,后來出現了一些輔助的庫
BRVAHXRecyclerView,它們可以很方便的實現Adapter的創建,Header/Footer,上拉加載等功能。

但隨著JetPack組件、Mvvm、ViewBinding等內容的更新,許多實現都可以進一步優化。

本文所研究的庫主要進行了以下的重點優化。

  • 使用ViewBinding簡化viewId,利用高階函數簡化Adapter創建
  • 使用ConcatAdapter,實現Footer,Header 等
  • 依賴倒置進行解耦,按需實現拓展,保存主庫精簡

項目地址 BindingAdapter

效果

實現一個普通Adapter:

我們不需要再創建Adapter 類,直接將Adapter創建在Activity中,也無需setItemClickListener,直接操作itemBinding即可

class XxActivity : Activity() {
    val adapter = BindingAdapter<ItemBean, ItemBinding>(ItemBinding::inflate) { position, item ->
        itemBinding.title.text = item.title
        itemBinding.title.setOnClickListener {
            deleteItem(item)
        }
    }
    fun deleteItem(item: ItemBean) {

    }
}


實現一個多布局Adapter:
同理,在Activity中通過buildMultiTypeAdapterByType方法

val adapter = buildMultiTypeAdapterByType {
    layout<String, ItemSimpleTitleBinding>(ItemSimpleTitleBinding::inflate) { _, item ->
        itemBinding.title.text = item
    }
    layout<Date, ItemSimpleBinding>(ItemSimpleBinding::inflate) { _, item ->
        itemBinding.title.text = item.toString()
    }
}

可以看到,通過BindingAdapter實現Adapter十分簡潔,只需要關注數據和視圖的綁定關系。

原理

一般創建一個原生Adapter 我們需要創建和實現class Adapter,class ViewHolderfun getItemCount()fun onCreateViewHolder()fun onBindViewHolder()

實際上,這些很多都是業務無關的模板代碼,因此我們可以對模板代碼進行簡化。

簡化ViewHolder的創建

ViewHolder是用來儲存列表的一個ItemView的容器,也是RecyclerView 回收的單位。

一般我們需要在ViewHolder創建時通過findViewById 獲取到各個View的引用進行保存,從而在onBindViewHolder時使用起來效率更高。

但是其繁瑣在于保存View引用需要以下操作:

  1. 需要定義變量
  2. 需要findViewById
  3. 需要保證xml中定義的類型和變量類型匹配,并且修改xml后,同步進行修改,沒有類型檢查容易造成運行時本庫

BRVAH 的方案是提供一個默認的ViewHolder,然后在onBindViewHolder時findViewById,并且使用緩存提高速度。確實簡化了許多,但是仍然存在操作2和3。

而在ViewBinding正是用來解決findViewById的,因此用ViewBinding結合ViewHolder以上問題都能完美解決,在此我們將不同的布局使用泛型去描述。

class BindingViewHolder<T : ViewBinding>(val adapter: RecyclerView.Adapter<*>, val itemBinding: T) :
    RecyclerView.ViewHolder(itemBinding.root) {

}

從此不再新建各種ViewHolder,在onCreateViewHolder()時直接新建BindingViewHolder<XxxBinding>即可。

Adapter 封裝

既然onCreateViewHolder都是固定的了,那我們將其他方法也解決了,就不用重寫各種方法了。

首先是Adapter的數據問題,95%的情況我們的數據都是一個List<T> ,4%的情況我們能通過自定義List類去實現,剩下1%的情況我還沒遇到。。。

因此我們直接使用kotlin 的List接口去描述列表數據。

所以getItemCount也直接代理給List.size實現了

接下來就是onBindViewHolder的解決,這個方法也是Adapter的核心作用,就是把一組Item 的屬性 轉換為一組View的屬性
比如:

    user.name   -> TextView.text
    user.type   -> TextView.color
    user.avatar -> ImageView.drawable

而有了ViewBinding后,View的屬性就使用布局的Binding類去控制,相當于只需要一個方法converter(item,viewBinding)
即可。

當然 ,有時候一個Adapter可能有不同的viewType,因此也會存在converter(item1,viewBinding1)converter(item2,viewBinding2)... 等,

也就是一個Adapter有1個或若干個converter

本著組合代替繼承的原則,我們另起一個抽象類ItemViewMapperStore去存儲這些converter,然后有2個實現類,分別對于1個和多個的情況(1個的單獨實現,不需要集合,可以省去查找過程,提升性能)。

將視圖相關的全部代理給itemViewMapperStore去實現,本庫的核心雛形已經出現了

open class MultiTypeBindingAdapter<I : Any, V : ViewBinding>(
    var itemViewMapperStore: ItemViewMapperStore<I, V>,
    list: List<I> = ArrayList(),
) : RecyclerView.Adapter<BindingViewHolder<V>>() {
    override fun onCreateViewHolder(
        parent: ViewGroup,
        viewType: Int,
    ): BindingViewHolder<V> = itemViewMapperStore.onCreateViewHolder(parent, viewType)

    override fun getItemViewType(position: Int) =
        itemViewMapperStore.getItemViewType(position, data[position])

    override fun onBindViewHolder(
        holder: BindingViewHolder<V>,
        position: Int,
        payloads: MutableList<Any>
    ) = itemViewMapperStore.onBindViewHolder(holder, position, payloads)

    override fun getItemCount() = data.size
}

實現ItemViewMapperStore

然后分別實現2種ItemViewMapperStore即可,他們的關系如下

雖然onCreateViewHolder都是產生BindingViewHolder,但是多類型的時候,我們不僅需要記錄converter還需要記錄泛型和構造器信息
使用 ItemViewMapper 包裝一下。

 class ItemViewMapper<I : Any, V : ViewBinding>(
    private val creator: LayoutCreator<V>,
    private val converter: LayoutConverter<I, V>
)

單類型Adapter的情況,沒有viewType,ItemViewMapper也只有一個。實現如下

open class SingleTypeItemViewMapperStore<I : Any, V : ViewBinding>(
    private val itemViewMapper: ItemViewMapperStore.ItemViewMapper<I, V>
) : ItemViewMapperStore<I, V> {
    override fun getItemViewType(position: Int, item: I) = 0
    override fun createViewHolder(
        adapter: RecyclerView.Adapter<*>,
        parent: ViewGroup,
        viewType: Int
    ): BindingViewHolder<V> = itemViewMapper.createViewHolder(adapter, parent)
    override fun bindViewHolder(
        holder: BindingViewHolder<V>,
        position: Int,
        item: I,
        payloads: List<Any>
    ) = itemViewMapper.bindViewHolder(holder, position, item, payloads)
}

多類型的情況,這里我們實現多種方式。

1. 原生方式

這種方式實現最簡單,相當于原生方式的簡易封裝,當然也最難用。(不推薦使用,可被方式2代替)

用法:需要先約定好布局id,通過extractItemViewType 指定布局id。通過layout定義布局id所對應的布局
適用情況:所有情況

val adapter = buildMultiTypeAdapterByMap<DataType> {
    val typeTitle = 0
    val typeNormal = 1
    layout(typeTitle, ItemSimpleTitleBinding::inflate) { _, item: DataType.TitleData ->
        itemBinding.title.text = item.text
    }
    layout(typeNormal, ItemSimpleBinding::inflate) { _, item: DataType.NormalData ->
        itemBinding.title.text = item.text
    }
    extractItemViewType { _, item -> if (item is DataType.TitleData) typeTitle else typeNormal }

}

原理:使用map保存type和layout的關系,然后onCreateViewHolder,onBindViewHolder中通過布局id取出layout調用,保存extractItemViewType里的高階函數,在getItemViewType中調用。
缺點:需要維護類型id,通過map查找效率一般。

2. 自動維護的布局類型

這種方式自動維護了布局類型id,而且內部使用數組,查找效率極高。

用法:通過layout定義布局,會生成布局id, 再通過extractItemViewType 指定布局id。
適用情況:所有情況

//2.自定義ItemType
val adapter = buildMultiTypeAdapterByIndex<DataType> {
    val typeTitle = layout(ItemSimpleTitleBinding::inflate) { _, item: DataType.TitleData ->
        itemBinding.title.text = item.text
    }
    val typeNormal = layout(ItemSimpleBinding::inflate) { _, item: DataType.NormalData ->
        itemBinding.title.text = item.text
    }
    extractItemViewType { position, item -> if (position % 10 == 0) typeTitle else typeNormal }

}

原理:使用數組保存layout,并用其下標作為布局id,然后onCreateViewHolder,onBindViewHolder中通過布局id取出layout調用,保存extractItemViewType里的高階函數,在getItemViewType中調用。
缺點:無。

3. 通過Item類型匹配布局

這種方式使用最簡單,也比較常用。
用法:通過layout定義布局
適用情況:不同布局的Item的類型也是不同的。

sealed class DataType(val text: String) {
    class TitleData(text: String) : DataType(text)
    class NormalData(text: String) : DataType(text)
}

val adapter =
    buildMultiTypeAdapterByType {
        layout<DataType.TitleData, ItemSimpleTitleBinding>(ItemSimpleTitleBinding::inflate) { _, item ->
            itemBinding.title.text = item.text
        }
        layout<DataType.NormalData, ItemSimpleBinding>(ItemSimpleBinding::inflate) { _, item ->
            itemBinding.title.text = item.text
        }
    }

原理:使用數組保存layout,并用其下標作為布局id,同時用map保存class和id的關系,然后onCreateViewHolder,onBindViewHolder中通過布局id取出layout調用,保存extractItemViewType里的高階函數,在getItemViewType中調用。
缺點:無

Header和Footer

本庫不含有Header和Footer的實現代碼,而是利用了RecyclerView的
ConcatAdapter

在此基礎上添加了一些拓展方法和類:

單個View的Adapter

使用SingleViewBindingAdapter1行代碼便能創建出單個View的Adapter。

它固定具有1個數據,一般可以用作Header,Footer。

val header = SingleViewBindingAdapter(HeaderSimpleBinding::inflate)

val header = SingleViewBindingAdapter(HeaderSimpleBinding::inflate) {
    //也可以配置布局內容
    itemBinding.tips.text = "ok"
}
//也可以后續更新布局內容
header.update {
    itemBinding.tips.text = "ok"
}

拷貝Adapter

使用copy() 拷貝一個Adapter,并使用其當前數據作為初始數據,后續的數據變更是相互獨立的,且狀態不共享。

原理十分簡單,就是使用當前itemViewMapperStore和數據新建一個Adapter。

fun <I : Any, V : ViewBinding> MultiTypeBindingAdapter<I, V>.copy(newData: List<I> = data): MultiTypeBindingAdapter<I, V> {
    return MultiTypeBindingAdapter(
        itemViewMapperStore,
        if (newData === data) ArrayList(data) else newData
    )
}

連接多個Adapter

可以使用+拓展方法依次連接多個Adapter,使ConcatAdapter更容易使用。
使用+添加的Adapter最終會添加到同一個ConcatAdapter中。

val header = SingleViewBindingAdapter(HeaderSimpleBinding::inflate)
val footer = SingleViewBindingAdapter(FooterSimpleBinding::inflate)

binding.list.adapter = header + adapter + footer

binding.list.adapter = header + adapter + header.copy() + adapter.copy() + footer //也可以任意拼接

控制Adapter的顯示和隱藏

通過adapter.isVisible控制Adapter 的顯示和隱藏,其實現非常簡單,就是通過isVisible屬性控制了item的數量為0實現隱藏。

    override fun getItemCount() = if (isVisible) data.size else 0

在結合ConcatAdapter時這十分有用,比如實現一個空布局,在有數據時隱藏,沒數據時顯示等等。

val adapter = BindingAdapter<ItemBean, ItemBinding>(ItemBinding::inflate) { position, item ->

}
val emptyLayoutAdapter = SingleViewBindingAdapter(FooterSimpleBinding::inflate)

fun init() {
    binding.list.adapter = adapter + emptyLayoutAdapter

    emptyLayoutAdapter.isVisible = false //隱藏
}

結合adapter原本的方法,能有更高的拓展性,無需更改Adapter內部實現空布局示例:

/**
 * 創建空布局
 * @param dataAdapter 數據源Adapter
 * @param text 沒有數據時顯示文案
 */
private fun emptyAdapterOf(
    dataAdapter: RecyclerView.Adapter<*>,
    text: String = "沒有數據"
): SingleViewBindingAdapter<FooterSimpleBinding> {
    val emptyAdapter =
        SingleViewBindingAdapter(FooterSimpleBinding::inflate) { itemBinding.tips.text = text }
    dataAdapter.registerAdapterDataObserver(object : RecyclerView.AdapterDataObserver() {
        override fun onChanged() {
            emptyAdapter.isVisible = dataAdapter.itemCount == 0
        }
        override fun onItemRangeInserted(positionStart: Int, itemCount: Int) = this.onChanged()
        override fun onItemRangeRemoved(positionStart: Int, itemCount: Int) = this.onChanged()
    })
    return emptyAdapter
}

//使用
binding.list.adapter = adapter + emptyAdapterOf(adapter)

拓展

許多Adapter庫/RecyclerView庫會在他們的庫中集成各種布局,動畫等,但絕大多少情況,我們都得按照設計稿來設計布局和動畫,而內置的東西不好改動和刪除。
所以在設計本庫的時候,我沒有內置很多東西,而是將接口暴露出來,來在不改動Adapter庫的情況下拓展我們的功能。

在此我們主要使用了依賴倒置的原則去解耦各種功能。

先看正向的依賴:在Adapter中依賴各個模塊,然后直接調用各個模塊的功能

import xx.PageModule

class BaseAdapter {
    var pageModule: PageModule? = null //分頁模塊

    fun onBindViewHolder() {
        pageModule.xxx()
    }
}

可以看到,BaseAdapter 依賴了PageModule,形成了耦合。但是項目中很多Adapter都不需要分頁模塊,如果模塊多了也存在著內存的浪費。

依賴倒置:主庫依賴于抽象,拓展模塊去實現各個抽象。

class BaseAdapter {
    val listeners: OnCreateViewHolderListeners
    fun addListener(listener: OnCreateViewHolderListener) {
    }
    fun onCreateViewHolder() {
        listeners.onBeforeCreateViewHolder()
        //...
        listeners.onAfterCreateViewHolder()
    }
}
class PageModule : OnCreateViewHolderListener {
    override fun onBeforeCreateViewHolder() {
    }
    override fun onAfterCreateViewHolder() {
    }
}

BindingAdapter中提供了許多可供攔截,監聽的方法,其實現也十分簡單,將原本的方法使用代理實現。

override fun onBindViewHolder(
    holder: BindingViewHolder<V>,
    position: Int,
    payloads: MutableList<Any>
) {
    onBindViewHolderDelegate(holder, position, payloads)
}
var onBindViewHolderDelegate: (holder: BindingViewHolder<V>, position: Int, payloads: List<Any>) -> Unit =
    { holder, position, payloads ->
        itemViewMapperStore.bindViewHolder(holder, position, data[position], payloads)
    }

為了更方便使用,我們提供了便捷的監聽方法

fun <V : ViewBinding> IBindingAdapter<V>.doAfterBindViewHolder(listener: (holder: BindingViewHolder<V>, position: Int) -> Unit): IBindingAdapter<V> {
    val onBindViewHolderDelegateOrigin = onBindViewHolderDelegate
    onBindViewHolderDelegate = { holder, position, p ->
        onBindViewHolderDelegateOrigin(holder, position, p)
        listener(holder, position)
    }
    return this
}
fun <V : ViewBinding> IBindingAdapter<V>.doBeforeBindViewHolder(listener: (holder: BindingViewHolder<V>, position: Int) -> Unit): IBindingAdapter<V> {
    val onBindViewHolderDelegateOrigin = onBindViewHolderDelegate
    onBindViewHolderDelegate = { holder, position, p ->
        listener(holder, position)
        onBindViewHolderDelegateOrigin(holder, position, p)
    }
    return this
}

同理還有interceptCreateViewHolderdoAfterCreateViewHolder

所以使用拓展方法實現監聽:

adapter.doBeforeBindViewHolder { holder, position ->
    holder.itemBinding.xxx=xxx
}

比如我們在嵌套RecyclerView時,內部的RecyclerView設置共用ViewPool可以提升復用減少內存消耗。

adapter.doAfterCreateViewHolder { holder, _, _ ->
    holder.itemBinding.orders.setRecycledViewPool(orderViewPool)
}

可見,通過依賴倒置,我們的Adapter沒有依賴任何拓展模塊的信息,而拓展模塊可以插入到主庫中實現拓展。

總結

通過ViewBinding 封裝了一個易拓展,低耦合的Adapter庫,使用極少的代碼便能完成1個Adapter,同時利用了官方自帶的ConcatAdapter實現了Header/Footer。

本著代碼越少,bug越少的原則,本庫保持十分精簡,核心代碼只有幾百行。

如果你的新項目已經使用了ViewBinding,那么BindingAdapter是不錯的選擇。

后續文章會更新分頁模塊,選擇模塊,滾輪模塊等文章。

更多內容也可以訪問項目主頁查看相關文檔

BindingAdapter

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容