前言
RecyclerView在項目中基本都是必備的了,
然而我們正常寫一個列表卻需要實現Adapter的onCreateViewHolder
,onBindViewHolder
,getItemCount
,以及需要ViewHolder
的眾多findViewById
。
這使得我們使用的成本大大增加,后來出現了一些輔助的庫
BRVAH、XRecyclerView,它們可以很方便的實現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 ViewHolder
,fun getItemCount()
,fun onCreateViewHolder()
,fun onBindViewHolder()
實際上,這些很多都是業務無關的模板代碼,因此我們可以對模板代碼進行簡化。
簡化ViewHolder的創建
ViewHolder是用來儲存列表的一個ItemView的容器,也是RecyclerView 回收的單位。
一般我們需要在ViewHolder創建時通過findViewById 獲取到各個View的引用進行保存,從而在onBindViewHolder時使用起來效率更高。
但是其繁瑣在于保存View引用需要以下操作:
- 需要定義變量
- 需要findViewById
- 需要保證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
使用SingleViewBindingAdapter
1行代碼便能創建出單個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
}
同理還有interceptCreateViewHolder
、doAfterCreateViewHolder
所以使用拓展方法實現監聽:
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是不錯的選擇。
后續文章會更新分頁模塊,選擇模塊,滾輪模塊等文章。
更多內容也可以訪問項目主頁查看相關文檔