RecyclerView封装-结合ViewBinding 3行代码创建Adapter!

前言

RecyclerView在项目中基本都是必备的了,
然而我们正常写一个列表却需要实现Adapter的onCreateViewHolder,onBindViewHolder,getItemCount
,以及需要ViewHolder的众多findViewById

这使得我们使用的成本大大增加,后来出现了一些辅助的库
BRVAH
XRecyclerView,它们可以很方便的实现Adapter的创建,Header/Footer,上拉加载等功能。

但随着JetPack组件、Mvvm、ViewBinding等内容的更新,许多实现都可以进一步优化。

本文所研究的库主要进行了以下的重点优化。

  • 使用ViewBinding简化viewId,利用高阶函数简化Adapter创建
  • 使用ConcatAdapter,实现Footer,Header 等
  • 依赖倒置进行解耦,按需实现拓展,保存主库精简

项目地址 BindingAdapter

效果

RecyclerView封装-结合ViewBinding 3行代码创建Adapter!_第1张图片

RecyclerView封装-结合ViewBinding 3行代码创建Adapter!_第2张图片

实现一个普通Adapter:

我们不需要再创建Adapter 类,直接将Adapter创建在Activity中,也无需setItemClickListener,直接操作itemBinding即可

class XxActivity : Activity() {
    val adapter = BindingAdapter(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(ItemSimpleTitleBinding::inflate) { _, item ->
        itemBinding.title.text = item
    }
    layout(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(val adapter: RecyclerView.Adapter<*>, val itemBinding: T) :
    RecyclerView.ViewHolder(itemBinding.root) {

}

从此不再新建各种ViewHolder,在onCreateViewHolder()时直接新建BindingViewHolder即可。

Adapter 封装

既然onCreateViewHolder都是固定的了,那我们将其他方法也解决了,就不用重写各种方法了。

首先是Adapter的数据问题,95%的情况我们的数据都是一个List ,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(
    var itemViewMapperStore: ItemViewMapperStore,
    list: List = ArrayList(),
) : RecyclerView.Adapter>() {
    override fun onCreateViewHolder(
        parent: ViewGroup,
        viewType: Int,
    ): BindingViewHolder = itemViewMapperStore.onCreateViewHolder(parent, viewType)

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

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

    override fun getItemCount() = data.size
}

实现ItemViewMapperStore

然后分别实现2种ItemViewMapperStore即可,他们的关系如下

RecyclerView封装-结合ViewBinding 3行代码创建Adapter!_第3张图片
虽然onCreateViewHolder都是产生BindingViewHolder,但是多类型的时候,我们不仅需要记录converter还需要记录泛型和构造器信息
使用 ItemViewMapper 包装一下。

 class ItemViewMapper(
    private val creator: LayoutCreator,
    private val converter: LayoutConverter
)

单类型Adapter的情况,没有viewType,ItemViewMapper也只有一个。实现如下

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

多类型的情况,这里我们实现多种方式。

1. 原生方式

这种方式实现最简单,相当于原生方式的简易封装,当然也最难用。(不推荐使用,可被方式2代替)

用法:需要先约定好布局id,通过extractItemViewType 指定布局id。通过layout定义布局id所对应的布局
适用情况:所有情况

val adapter = buildMultiTypeAdapterByMap {
    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 {
    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(ItemSimpleTitleBinding::inflate) { _, item ->
            itemBinding.title.text = item.text
        }
        layout(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  MultiTypeBindingAdapter.copy(newData: List = data): MultiTypeBindingAdapter {
    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(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 {
    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,
    position: Int,
    payloads: MutableList
) {
    onBindViewHolderDelegate(holder, position, payloads)
}
var onBindViewHolderDelegate: (holder: BindingViewHolder, position: Int, payloads: List) -> Unit =
    { holder, position, payloads ->
        itemViewMapperStore.bindViewHolder(holder, position, data[position], payloads)
    }

为了更方便使用,我们提供了便捷的监听方法

fun  IBindingAdapter.doAfterBindViewHolder(listener: (holder: BindingViewHolder, position: Int) -> Unit): IBindingAdapter {
    val onBindViewHolderDelegateOrigin = onBindViewHolderDelegate
    onBindViewHolderDelegate = { holder, position, p ->
        onBindViewHolderDelegateOrigin(holder, position, p)
        listener(holder, position)
    }
    return this
}
fun  IBindingAdapter.doBeforeBindViewHolder(listener: (holder: BindingViewHolder, position: Int) -> Unit): IBindingAdapter {
    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

你可能感兴趣的:(RecyclerView封装-结合ViewBinding 3行代码创建Adapter!)