前言
在上一篇 如何快速实现一个列表组件中我们通过借助开源库 fast-list 了解到了如何快速用 RecyclerView 实现列表功能。但是 fast-list 的实现是有效率问题的,日常写 demo 测试没有什么影响,但是如果想直接在生产环境使用,还是需要做一些优化的,下面就来看看具体的问题及优化方式。
为什么需要 ViewHolder
首先我们思考一个问题,在实现 RecyclerView 的 Adapter 时为什么需要一个 ViewHolder,他的作用是什么?
class FruitAdapter(val fruitList: List<Fruit>) : RecyclerView.Adapter<FruitAdapter.ViewHolder>() {
inner class ViewHolder(view: View) : RecyclerView.ViewHolder(view) {
val fruitImage: ImageView = view.findViewById(R.id.fruitImage)
val fruitName: TextView = view.findViewById(R.id.fruitName)
}
...
}
对于这个问题,如果使用过 ListView ,自然是非常清楚的。ViewHolder 顾名思义,提供了 view 的缓存机制,提前将列表项的组件通过 findViewById 的形式完成初始化。 在列表滚动执行 onBindViewHolder 给 UI 绑定数据的时候,避免了频繁调用 findViewById ,导致无谓的性能损耗。
fast-list
我们回顾一下 fast-list 的用法和源码
val datas: ArrayList<String> = ArrayList()
recyclerView.bind(datas,R.layout.list_item_avatar, object : (View, String, Int) -> Unit {
override fun invoke(p1: View, item: String, position: Int) {
val title: TextView = p1.findViewById(R.id.title_tv)
val index: TextView = p1.findViewById(R.id.index_tv)
title.text = item
index.text = position.toString()
}
})
class FastListViewHolder<T>(val containerView: View, val holderType: Int) : RecyclerView.ViewHolder(containerView) {
fun bind(entry: T, position: Int, func: BindingClosure<T>) {
containerView.apply {
func(entry, position)
}
}
}
可以看到,onBindViewHolder 执行时调用了 FastListViewHolder 的 bind 方法,而具体执行时,是以当前 View 作为接收者进行调用的,因此在执行 BindingClosure 的具体实现时,可以通过传入的参数获取直接进行 findViewById 的操作。或者说我们只能这么做,因为只有这一个对外暴露的方法。
也就是说这里 FastListViewHolder 没有体现出作为一个 ViewHolder 该有的作用。从本质上看 fast-list 本身的设计框架就注定了会有这种问题。在 fast-list 中 Adapter 的泛型参数是 FastListViewHolder ,这样就导致内部所有用到 ViewHolder 的地方,只能和这一种 ViewHolder 通信。因此,我们需要将创建 ViewHolder 的流程抽象到更高的层次,使得调用者可以创建自己的 ViewHolder,只有这样才可以让 ViewHolder 名副其实,起到真正的作用。
优化 fast-list
为了解决频繁调用 findViewById 的问题,我们从 FastListViewHolder 开始动手。
可扩展的 FastListViewHolder
open class FastListViewHolder<T>(containerView: View, val holderType: Int) :
RecyclerView.ViewHolder(containerView) {
fun bind(entry: T, position: Int, bind: BindingClosure<T>) {
this.bind(entry, position)
}
}
- 首先给 FastListViewHolder 添加 open 修饰符,确保其可以被继承。因为,我们需要创建自己的 ViewHolder
- bind 方法执行时接收者由 View 修改为 this,也就是 ViewHolder 自身,这样 bind 方法执行时就是以 ViewHolder 作为接收者,而不再是 View .
创建 ViewHolder
typealias BindingClosure<T> = (FastListViewHolder<T>.(item: T, position: Int) -> Unit)
typealias CreateClosure<T> = (View, Int) -> FastListViewHolder<T>
- 基于 FastListViewHolder 内的修改,同步修改 BindingClosure 的接收者,由原先的 View 修改为 FastListViewHolder
- 同时新建一个高阶函数的表达式 CreateClosure,用于动态创建 ViewHolder 。
fun <T> RecyclerView.bind(
items: List<T>,
@LayoutRes singleLayout: Int = 0,
create: CreateClosure<T>,
singleBind: BindingClosure<T>
): FastListAdapter<T> {
layoutManager = LinearLayoutManager(context)
return FastListAdapter(
items.toMutableList(), this
).map(singleLayout, { item: T, position: Int -> true }, create, singleBind)
}
bind 方法新增一个参数, create: CreateClosure<T> 用于聚合创建 ViewHolder 的实现。同时将这个参数通过 map 方法透传到 BindMap 的构造函数中。这样 BindMap 在原有的基础上,又聚合了创建 ViewHolder 的能力。
最后,修改 onCreateViewHolder 内创建 ViewHolder 的实现,
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): FastListViewHolder<T> {
return bindMap.first { it.type == viewType }.let { k ->
val view = LayoutInflater.from(parent.context).inflate(k.layout, parent, false)
k.create(view, viewType)
}
}
对比之前的实现方式
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): FastListViewHolder<T> {
return bindMap.first { it.type == viewType }.let {
val view = LayoutInflater.from(parent.context).inflate(it.layout,parent, false)
return FastListViewHolder(view, viewType)
}
}
通过对比可以看到,相比之前在内部直接创建 FastListViewHolder 的方式,这里修改为由 create 这个高阶函数来进行创建,这样就更灵活了。
自定义 ViewHolder
既然 ViewHolder 创建的方式已经对外暴露了,那么我们就可以自定义 ViewHolder 了
class MyHolder(container: View, viewType: Int) :
FastListViewHolder<String>(container, viewType) {
val title: TextView = container.findViewById(R.id.title_tv)
val index: TextView = container.findViewById(R.id.index_tv)
}
val datas: ArrayList<String> = ArrayList()
recyclerView.bind(datas,R.layout.list_item_avatar,
{ container: View, viewType: Int -> MyHolder(container, viewType) },
{ item: String, pos: Int ->
if (this is MyHolder) {
this.title.text = item
this.index.text = pos.toString()
}
})
这样就解决了 ViewHolder 内频繁调用 findViewById 的问题。不过这里由于 Adapter 泛型参数固定的问题,BindingClosure 的接收者依然是 FastListViewHolder , 绑定数据时需要手动做一次类型转换。当然,这个问题可以通过以下方式得到解决。
数据直接绑定
既然 ViewHolder 的实例都可以自行创建了,我们可以更激进一些,直接将数据绑定的操作逻辑在 ViewHolder 中实现。
稍微修改一下 FastListViewHolder 的实现
open class FastListViewHolder<T>(containerView: View, val holderType: Int) :
RecyclerView.ViewHolder(containerView) {
fun bind(entry: T, position: Int, bind: BindingClosure<T>?) {
bind?.let {
this.it(entry,position)
} ?: run {
bind(entry,position)
}
}
open fun bind(entry: T,position: Int) {}
}
将高阶函数 BindingClosure 的类型修改为可空,同时添加一个子类可以覆盖的 bind 方法,这样就可以更灵活了。
class MyHolder1(container: View, viewType: Int) :
FastListViewHolder<String>(container, viewType) {
val title: TextView = container.findViewById(R.id.title_tv)
val index: TextView = container.findViewById(R.id.index_tv)
override fun bind(entry: String, position: Int) {
super.bind(entry, position)
title.text = "$entry @ holder 1"
index.text = position.toString()
}
}
recyclerView.bind(datas, R.layout.list_item,
{ container, viewType -> MyHolder1(container, viewType) })
通过覆写 bind 方法将 ViewHolder 数据绑定的逻辑直接在其内部实现,省略了 BindingClosure 这个高阶函数。当然,如果传入高阶函数的话,依然可以生效。
这样优化之后,从语法上更简洁了,RecyclerView 调用 bind 方法时只需要提供数据、布局文件、ViewHolder ,单从写法的角度出发,可以说和 Compose 一样简单了。
@Composable
fun ListItem(text: String) {
Text(text = text, modifier = Modifier.padding(16.dp))
}
@Composable
fun ScrollableList() {
LazyColumn {
items(items) { item ->
ListItem(text = item)
}
}
}
同时对于不同类型的列表项,我们可以自由创建不同的 Viewholder 进行组合。
class MyHolder2(container: View, viewType: Int) :
FastListViewHolder<String>(container, viewType) {
val title: TextView = container.findViewById(R.id.title_tv)
val index: TextView = container.findViewById(R.id.index_tv)
override fun bind(entry: String, position: Int) {
super.bind(entry, position)
title.text = "$entry @ holder 2"
index.text = (position * position).toString()
}
}
recyclerView.bind(datas).map(R.layout.list_item_avatar,
{ _: String, pos: Int -> pos < 5 },
{ container, viewType -> MyHolder1(container, viewType) })
.map(R.layout.list_item,
{ _, pos -> pos >= 5 },
{ container: View, viewType: Int -> MyHolder2(container, viewType) })
调用 map 方法有三个参数
- 布局文件
- 根据数据和位置确定返回一个 boolean 值,这里其实就是在写 getItemViewType 内的逻辑。
- 提供当前这个布局文件对应的 ViewHolder
这样基本上就可以满足对于 RecyclerView 进行数据绑定的所有操作了。
完整代码可参考 GitHub
小结
从 fast-list 本身存在的小问题出发,在解决问题的过程中,通过将 ViewHolder 的创建过程进行抽象,让整个框架更加灵活了。再一次感受到 Kotlin 高阶函数的魅力,对函数是第一公民这个理念有了更深入的理解。