前言

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!
RecyclerView封装-结合ViewBinding 3行代码创建Adapter!

完结一个一般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非常简洁,只需求重视数据和视图的绑定联络。

ViewBinding 简介

可能一些读者还没用过ViewBinding,在此简略介绍下ViewBinding 视图绑定

因为在代码中findViewById的繁琐,所以也呈现了一些的优秀的库ButterKnifeKotlin-Android-Extention 来简化findView的操作。可是仍然有一些缺乏,比方空安全和类型安全的问题。于是推出了ViewBinding

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

面试官也很喜欢问ViewBinding、DataBinding的差异和联络,这儿官方也有写明:

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

实际上,只需求记住ViewBinding只是一个替代findViewById的东西。 而DataBinding是ViewBinding的子集增加了一些绑定功用,因而本库也适用于DataBinding,用法也是相同的。

原理

一般创立一个原生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。

将视图相关的悉数署理给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即可,他们的联络如下

RecyclerView封装-结合ViewBinding 3行代码创建Adapter!
虽然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)
}

多类型Adapter的状况,运用集合存储每个viewType对应的ItemViewMapper。

根据适用范围的不同,这儿咱们提供完结多种办法。

1. 原生办法

这种办法完结最简略,相当于原生办法的简易封装,当然也最难用。(不引荐运用,可被办法2替代)

  • 用法:需求先约定好布局id,经过extractItemViewType 指定布局id。经过layout界说布局id所对应的布局

  • 适用状况:所有状况

  • 原理:运用map保存type和layout的联络,然后onCreateViewHolder,onBindViewHolder中经过布局id取出layout调用,保存extractItemViewType里的高阶函数,在getItemViewType中调用。

  • 缺陷:需求保护类型id,经过map查找效率一般。

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 }
}

2. 主动保护的布局类型

这种办法主动保护了布局类型id,而且内部运用数组,查找效率极高。

  • 用法:经过layout界说布局,会生成布局id, 再经过extractItemViewType 指定布局id。

  • 适用状况:所有状况

  • 原理:运用数组保存layout,并用其下标作为布局id,然后onCreateViewHolder,onBindViewHolder中经过布局id取出layout调用,保存extractItemViewType里的高阶函数,在getItemViewType中调用。

  • 缺陷:无。

//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 }
}

3. 经过Item类型匹配布局

这种办法运用最简略,也比较常用。

  • 用法:经过layout界说布局
  • 适用状况:不同布局的Item的类型也是不同的。
  • 原理:运用数组保存layout,并用其下标作为布局id,一起用map保存class和id的联络,然后onCreateViewHolder,onBindViewHolder中经过布局id取出layout调用,保存extractItemViewType里的高阶函数,在getItemViewType中调用。
  • 缺陷:无
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
        }
    }

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越少的准则,本库保持非常精简,中心代码只要几百行。

本库要点在于对原生Adapter中运用findViewById、ButterKnife的一种升级计划,

假如你的项目现已运用了ViewBinding,或准备用ViewBinding,或者想简化旧项目代码,那么BindingAdapter是不错的选择。

后续文章会更新分页模块,选择模块,滚轮模块等文章。

更多内容也能够拜访项目主页查看相关文档

BindingAdapter