前言
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非常简洁,只需求重视数据和视图的绑定联络。
ViewBinding 简介
可能一些读者还没用过ViewBinding,在此简略介绍下ViewBinding 视图绑定
因为在代码中findViewById的繁琐,所以也呈现了一些的优秀的库ButterKnife、Kotlin-Android-Extention 来简化findView的操作。可是仍然有一些缺乏,比方空安全和类型安全的问题。于是推出了ViewBinding
面试官也很喜欢问ViewBinding、DataBinding的差异和联络,这儿官方也有写明:
实际上,只需求记住ViewBinding只是一个替代findViewById的东西。 而DataBinding是ViewBinding的子集增加了一些绑定功用,因而本库也适用于DataBinding,用法也是相同的。
原理
一般创立一个原生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。
将视图相关的悉数署理给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)
}
多类型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
运用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越少的准则,本库保持非常精简,中心代码只要几百行。
本库要点在于对原生Adapter中运用findViewById、ButterKnife的一种升级计划,
假如你的项目现已运用了ViewBinding,或准备用ViewBinding,或者想简化旧项目代码,那么BindingAdapter是不错的选择。
后续文章会更新分页模块,选择模块,滚轮模块等文章。
更多内容也能够拜访项目主页查看相关文档
BindingAdapter