美文网首页
RecyclerView封装-结合ViewBinding 3行代

RecyclerView封装-结合ViewBinding 3行代

作者: luowenbin | 来源:发表于2023-05-31 18:29 被阅读0次

    前言

    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

    相关文章

      网友评论

          本文标题:RecyclerView封装-结合ViewBinding 3行代

          本文链接:https://www.haomeiwen.com/subject/hgvyedtx.html