用 fast-list 更高效创建列表

453 阅读5分钟

前言

在上一篇 如何快速实现一个列表组件中我们通过借助开源库 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)
    }
}
  1. 首先给 FastListViewHolder 添加 open 修饰符,确保其可以被继承。因为,我们需要创建自己的 ViewHolder
  2. 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>
  1. 基于 FastListViewHolder 内的修改,同步修改 BindingClosure 的接收者,由原先的 View 修改为 FastListViewHolder
  2. 同时新建一个高阶函数的表达式 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 方法有三个参数

  1. 布局文件
  2. 根据数据和位置确定返回一个 boolean 值,这里其实就是在写 getItemViewType 内的逻辑。
  3. 提供当前这个布局文件对应的 ViewHolder

这样基本上就可以满足对于 RecyclerView 进行数据绑定的所有操作了。

完整代码可参考 GitHub

小结

fast-list 本身存在的小问题出发,在解决问题的过程中,通过将 ViewHolder 的创建过程进行抽象,让整个框架更加灵活了。再一次感受到 Kotlin 高阶函数的魅力,对函数是第一公民这个理念有了更深入的理解。

参考文档

kotlin-android-extensions插件也被废弃了?扶我起来