市面上已经有不少开源且好用的 Adapter
库,为啥非要本身造轮子呢?
java
学习和工做中我经常使用过 BaseRecyclerViewAdapterHelper 和 SugarAdapter,也都实现过复杂的列表,倒也不能说很差用或者不顺手,只是和本身的理念不太匹配,一个简单的 Adapter
为啥都要搞得那么复杂呢,因而决定按照本身的想法造一个简单高效的轮子。git
Adapter
适配器,顾名思义,是两类不一样东西之间沟通的桥梁,RecyclerView.Adapter
则是为了实现数据到视图的适配,说白了就是一种 Model -> View
的映射关系。
本着「单一职责」的原则,这里造的轮子只考虑 Adapter
的本职工做,不考虑添加什么「头部」、「尾部」之类的东西,简单高效地实现 Model -> View
的映射。github
这里先放下 Demo 中的截图,Demo 中会有 6 种卡片样式, 4 种数据类型和 3 种自定义的 ViewHolder。
缓存
这里主要讲述下实现的一些思路,和大体的实现,无论兴趣的能够直接看 Demo 的演示。bash
RecyclerView.ViewHolder
承载着渲染数据的视图,可是没有对数据作任何封装。这里最基本的加个数据的泛型,缓存下当前的数据,提供一个 onBind
方法来接收 onBindViewHolder
时的数据。这样就实现了 SimpleAdapter 的第一个类 SimpleHolder
,至于布局文件和渲染逻辑,以后再说。架构
open class SimpleHolder<T: Any>(v: View) : RecyclerView.ViewHolder(v) {
/**
* holder 的当前数据
*/
var data: T? = null
@CallSuper
open fun onBind(data: T) { this.data = data }
//...
}
复制代码
这里的「一条映射关系」指的是:我拿到了一个数据 T
-> 根据 T
里的数据确认下要渲染成什么样的视图 -> 这个视图用什么布局文件 -> 数据的渲染逻辑用什么 ViewHolder
。也就是说「一条映射关系」是一种 ViewHolder
对某一类数据的渲染方式。因为 RecyclerView
对 ViewHolder
缓存是以 getItemViewType
的值为 key 的进行的,所以「一条映射关系」的配置须要与惟一的 viewType
相匹配。maven
这里须要定义一个配置项,来配置一条映射关系,在定义以前,先来考虑下映射过程当中都涉及到哪些东西:工具
Class<T>
(T)->Boolean
LayoutRes
ViewHolder
:Class<out SimpleHolder<T>>
RecyclerView
的缓存机制,这里也要惟一肯定一个:viewType
ViewHolder
建立时作一些通用逻辑:(SimpleHolder<T>)->Unit
onBindViewHolder
时:(SimpleHolder<T>, T)->Unit
这些就组成了 SimpleAdapter 的第二个类 HolderInfo
,这里就再也不贴代码了。布局
SimpleAdapter
里的实现这是 SimpleAdapter 里的最后一个类,和其余 Adapter
同样,SimpleAdapter
也封装了一个 List<Any>
存放列表数据,泛型 Any
是为了避免对数据类型作约束,支持不一样类型数据的映射和渲染。性能
fun getItemCount(): Int
返回 list.size
fun getItemViewType(position: Int): Int
会根据对应位置的数据拿到映射信息 HolderInfo
,返回它的 viewType
fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SimpleHolder<Any>
再根据 viewType
拿到 HolderInfo
,进而拿到布局文件和 ViewHolder
类型等数据,进而实例化出 SimpleHolder<Any>
fun onBindViewHolder(holder: SimpleHolder<Any>, position: Int)
触发 SimpleHolder.onBind
和对应 HolderInfo.onCreate
HolderInfo
拿到 HolderInfo
的方式有两种,一种是 getItemViewType
根据当前数据拿到对应支持的 HolderInfo
,另外一种就是 onCreateViewHolder
和 onBindViewHolder
须要根据 viewType
拿到 HolderInfo
。
所以须要一个以数据类型 Class
为 key,MutableList
为 value 的 Map
,value 是个列表是由于同一类型的数据可能对应多个 HolderInfo
。另外一个是以 viewType
为 key,HolderInfo
为 value 的 SparseArray
。
private val infoByDataClass = HashMap<Class<Any>, MutableList<HolderInfo<Any>>>()
private val infoByType = SparseArray<HolderInfo<Any>?>()
复制代码
typealias OnCreateHolder<T> = (SimpleHolder<T>)->Unit
typealias OnBindHolder<T> = (SimpleHolder<T>, T)->Unit
/** * 监听 onCreateViewHolder */
val onCreateListeners = ArrayList<OnCreateHolder<Any>>(2)
/** * 监听 onBindViewHolder */
val onBindListeners = ArrayList<OnBindHolder<Any>>(2)
复制代码
代码有问题,直接崩溃才能让咱们及时发现,可是在线上的崩溃会极大影响用户体验,出现异常状况,咱们但愿可以有必定的降级机制来处理,这里对一些主要场景的问题提供了降级处理的方法。
/** * getItemViewType 遇到不支持的数据类型时的出错处理,不设置会抛出异常 */
var onGetViewTypeError: ((SimpleAdapter, Int)->Int)? = null
复制代码
SimpleAdapter
里可能放入不支持的数据类型,致使 getItemViewType
没法处理,这时候就会触发这个回调,让使用者决定返回什么 viewType
。
/** * onCreateViewHolder 的出错处理,能够返回自定义的 SimpleHolder 来显示错误信息 * 不设置或者返回 null 会从新抛出异常 */
var onCreateError: ((SimpleAdapter, Exception, ViewGroup, Int)->SimpleHolder<Any>?)? = null
复制代码
建立 ViewHolder
时出错可让使用者提供一个降级的 ViewHolder
,好比渲染一个显示出错信息的视图等。
/** * onBindViewHolder 的出错处理,不设置或者返回 null 会从新抛出异常 */
var onBindError: ((SimpleAdapter, Exception, SimpleHolder<Any>, Int)->Unit)? = null
复制代码
onBindViewHolder
时出错也提供一样的降级处理的机会,好比把整个视图隐藏掉。
SimpleAdapter(list)
.addHolderInfo(/* ... */)
.addHolderInfo(/* ... */)
// ...
复制代码
SimpleAdapter
的构建很简单,只须要传入列表就行,以后添加须要的 HolderInfo
就能完成数据到视图的渲染,渲染方式下面细说。
addHolderInfo(
HolderInfo(
LoadingMore::class.java, R.layout.holder_loading_more ) ) 复制代码
这是最简单的建立 HolderInfo
的方式,会把某一类型的数据映射到布局文件建立的视图,因为没有设置任何回调,没法变动视图,所以只适用于视图固定的场合。上面的配置会渲染成「正在加载......」。
addHolderInfo(
HolderInfo(
TitleLine::class.java, R.layout.holder_title_line, onCreate = { holder ->
holder.itemView.setOnClickListener { v ->
val toast = holder.data?.toast ?: return@setOnClickListener
v.context.toast(toast)
}
},
onBind = { holder, data ->
holder.v<TextView>(R.id.tv_title)?.text = data.title
holder.v<TextView>(R.id.tv_info)?.text = data.info
}
)
)
复制代码
一些简单的卡片,不必继承 SimpleHolder
单独实现一个类,能够经过相似这种方式在 onCreate
和 onBind
中作些统一的渲染逻辑。Demo 中的「即将上线」、「热门电影」等就是用这种方式渲染的。
ViewHolder
addHolderInfo(
HolderInfo(
Movie::class.java,
R.layout.holder_online_movie,
OnlineMovieViewHolder::class.java,
isSupport = { it.isOnline }
)
)
复制代码
通常卡片逻辑都比较复杂,会单独实现一个 ViewHolder
,这里须要继承自 SimpleAdapter
。因为一种数据类型可能渲染成多种卡片,还可能须要用 isSupport
过滤,固然 onCreate
和 onBind
也是能够用来处理统一逻辑的,好比设置一些 listener 等。Demo 中的在线电影、即将上映的电影和推荐的电影都是这种方式实现的。
onGetViewTypeError = { adapter, position ->
sceneContext?.toast("不支持的数据: ${adapter.list[position]}")
0
}
onCreateError = { _, _, parent, viewType ->
val v = LayoutInflater.from(parent.context).inflate(R.layout.holder_error, parent, false)
val holder = SimpleHolder<Any>(v)
holder.v<TextView>(R.id.tv_info)?.text = "不支持的 viewType: $viewType"
holder
}
复制代码
Demo 中 onGetViewTypeError
会弹出 toast 提示,onCreateError
会显示一个出错的视图。
Demo 中的布局方式用的是 GridLayoutManager
,用起来也很简单,再也不赘述。
ListViewModel
中会处理刷新和加载更多的逻辑,这里简单处理了「空闲」、「刷新中」和「正在加载更多」三种状态。
「正在加载更多」的实现,须要配置 HolderInfo
指定下渲染逻辑,在加载时往列表里添加一个对应的数据,加载完再删掉就行。加载更多的触发用的是 RecyclerViewLoadMore
,原理就是 RecyclerView.addOnScrollListener
,再列表接近底部时触发回调,加载更多。
列表的更新用了 LiveList
,是一个封装了列表操做和 Adapter
更新的类,就不用手动 notifyXxx
了。
RecyclerView
不论是性能仍是架构设计,都很优秀,Recycler
处理缓存,LayoutManager
处理布局,ItemDecoration
处理卡片的装饰,ItemAnimator
处理动画,各个类的职责分离且明确,很是值得咱们学习。SimpleAdapter
只处理数据到视图的映射,什么头部卡片,底部卡片,都是数据层面的逻辑,只用变动列表数据,Adapter
只要考虑简单高效地解决 adapter 的逻辑就行。
Demo 和工具类的库都在 github.com/funnywolfda… 中,以后还会不断地更新一些本身经常使用的工具,总结并补充相应 demo,依赖这个库只须要:
allprojects {
repositories {
maven { url "https://jitpack.io" }
}
}
dependencies {
implementation 'com.github.funnywolfdadada:HollowKit:1.0'
}
复制代码