前言
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(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 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(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即可,他们的关系如下
虽然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
使用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 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
}
同理还有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是不错的选择。
后续文章会更新分页模块,选择模块,滚轮模块等文章。
更多内容也可以访问项目主页查看相关文档