是时候提高你撸RecycleView的效率了

作者: ea14cffb33a4 | 来源:发表于2020-07-09 20:17 被阅读0次

    一名优秀的Android开发,需要一份完备的 知识体系,在这里,让我们一起成长为自己所想的那样~。

    前言

    在项目开发中,总离不开列表,说到列表,就会有无穷无尽的 Adapter 需要你去实现。从而出现了很多优秀的 adapter 库。 不过很多库功能都很全面了,有很多个类,很多功能,但是实际上只用其中的一两个我感觉是很多人的现状。所以自己实现一个使用起来顺手的是一个不错的选择,特别是用在一些不是很复杂的列表时。

    效果

    先看看使用效果,激发一下你为数不多继续阅读的激情:

    //单类型列表,默认 LinearLayoutManager
    recycleView.setup<NumberInfo> {
        dataSource(initData())
        adapter {
            addItem(R.layout.layout_item) {
                bindViewHolder { data, _, _ ->
                    setText(R.id.number, data?.number.toString())
                }
            }
        }
    }
    
    //多类型列表
    recycleView.setup<Any> {
        withLayoutManager { LinearLayoutManager(context) }
        dataSource(data)
        adapter {
            addItem(R.layout.item_setion_header) {
                isForViewType { data, _ -> data is SectionHeader }
                bindViewHolder { data, _, _ ->
                    val header = data as SectionHeader
                    setText(R.id.section_title, header.title)
                }
            }
            addItem(R.layout.item_user) {
                isForViewType { data, _ -> data is User }
                bindViewHolder { data, _, _ ->
                    val user = data as User
                    setText(R.id.name, user.name)
                    setImageResource(R.id.avatar, user.avatarRes)
                    //如果你的控件找不到方便赋值的方法,可以通过 findViewById 去查找
                    val phone = findViewById<TextView>(R.id.phone)
                    phone.text = user.phone
                }
            }
        }
    }
    
    

    嗯....,感觉还可以,最少的情况下可以把一个列表代码用 10 几行就完成了。

    完整代码地址

    先贴完整代码地址,没地址的文章是没灵魂的:EfficientAdapter
    我把它命名为 EfficientAdapter ,意为高效的意思,事实上它只有 3 个文件。
    至于如何使用,在地址上已经描述了,所以这篇文章主要是讲一下实现的思路。

    实现思路

    对 Adapter 的封装,其实无非就是对 Adapter 里面的几个回调方法进行封装罢了,最常用的方法是先定义好一个存放 ViewHolder 的列表,然后在各个回调中获取这些 ViewHolder,然后实现逻辑。

    那么其中最操蛋的是哪个回调方法的封装呢?我认为是 getItemViewType。事实上你可以在很多框架中看到让你实现获取 ViewType 的回调方法。

    一步一步来,先说 ViewHolder 的封装

    在 EfficientAdapter 里面,我把 ViewHolder 的封装写成了 BaseViewHolder:

    class BaseViewHolder(parent: ViewGroup, resource: Int) : RecyclerView.ViewHolder(
            LayoutInflater.from(parent.context).inflate(resource, parent, false)
    )
    

    这就是我的封装,够简单吧。

    想什么呢,当然没这么简单,想要在上面使用效果代码中那样实现 ViewHolder 的具体逻辑,还需要有 isForViewType,bindViewHolder 等方法。所以我要定义一个类,去提供这些方法:

    abstract class ViewHolderCreator<T> {
        abstract fun isForViewType(data: T?, position: Int): Boolean
        abstract fun getResourceId(): Int
        abstract fun onBindViewHolder(
                data: T?, items: MutableList<T>?,
                position: Int,  holder: ViewHolderCreator<T>
        )
    
        var itemView: View? = null
    
        fun registerItemView(itemView: View?) {
            this.itemView = itemView
        }
    
        fun <V : View> findViewById(viewId: Int): V {
            checkItemView()
            return itemView!!.findViewById(viewId)
        }
    
        private fun checkItemView() {
            if (itemView == null) {
                throw NullPointerException("itemView is null")
            }
        }
    }
    

    在 ViewHolderCreator 中,getResourceId 和 onBindViewHolder 方法相信都知道是干嘛的,而 isForViewType 方法是用来判断 ViewType 的,注意它返回类型是 Boolean,这个方法会在下面讲到。因为我想在 onBindViewHolder 中能方便的拿到 view,所以有了 registerItemView 和 findViewById 等其他方法。

    以上就是 ViewHolder 的所有封装,接下来就对 Adapter 的封装。

    open class EfficientAdapter<T> : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
        var items: MutableList<T>? = mutableListOf()
        private val typeHolders: SparseArrayCompat<ViewHolderCreator<T>> = SparseArrayCompat()
    }
    

    Adapter 首先需要一个泛型用来表示传入的实体类类型,定义了一个 item 列表用来做数据源。ViewHolder 的集合使用一个 SparseArrayCompat 去存储。之所以用 SparseArray ,是因为我想把 ViewType 做为 key。

    所以,在 onCreateViewHolder 回调方法中,需要根据 viewType 参数在 typeHolders 中取到具体的 ViewHolderCreator:

    private fun getHolderForViewType(viewType: Int): ViewHolderCreator<T>? {
        return typeHolders.get(viewType)
    }
    
    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
        val holder = getHolderForViewType(viewType)
                ?: throw NullPointerException("No Holder added for ViewType $viewType")
        return BaseViewHolder(parent, holder.getResourceId())
    }
    

    这样,就可以通过 getHolderForViewType 方法,在 typeHolders 中获取到对应的 ViewHolderCreator,然后根据 ViewHolderCreator 中的信息去创建一个新的 ViewHolder。如果找不到,就抛一个空指针异常。

    同样道理,onBindViewHolder 回调方法也可以这么做:

    override fun onBindViewHolder(viewHolder: RecyclerView.ViewHolder, position: Int) {
        onBindViewHolder(viewHolder, position, mutableListOf())
    }
    
    override fun onBindViewHolder(viewHolder: RecyclerView.ViewHolder, position: Int, payloads:MutableList<Any>) {
        val holder = getHolderForViewType(viewHolder.itemViewType)
                ?: throw NullPointerException("No Holder added for ViewType " + viewHolder.itemViewType)
        holder.registerItemView(viewHolder.itemView)
        holder.onBindViewHolder(items?.get(position), items, position, holder)
    }
    

    注意的是 onBindViewHolder 回调方法有两个,他们的区别就不说了,这里两个都实现了逻辑,当然你也可以只实现一个。

    还剩下 getItemCount 和 getItemViewType 回调方法了,getItemCount 其实没什么好说的:

    override fun getItemCount(): Int = items?.size ?: 0
    

    先不说如何实现 getItemViewType ,先说说怎么添加数据到 typeHolders 中:

    fun register(holder: ViewHolderCreator<T>) = apply {
        var viewType: Int = typeHolders.size()
        while (typeHolders.get(viewType) != null) {
            viewType++
        }
        typeHolders.put(viewType, holder)
    }
    

    typeHolders 的类型是 SparseArrayCompat,这里我用 ViewType 作为 key,register 方法中,可以看到没每注册一次,viewType 就自动加一(因为 typeHolders 的长度会变长),达到了不会重复的效果,到时候在实现 getItemViewType 的时候,就直接取出来即可。避免了具体业务的干扰。

    最后看看 getItemViewType 的实现:

    override fun getItemViewType(position: Int): Int {
        if (items == null) {
            throw NullPointerException("adapter data source is null")
        }
        for (i in 0 until typeHolders.size()) {
            val holder = typeHolders.valueAt(i)
            val data = items?.getOrNull(position)
            if (holder.isForViewType(data, position)) {
                return typeHolders.keyAt(i)
            }
        }
    
        //找不到匹配的 viewType
        throw NullPointerException(
                "No holder added that matches at position=$position in data source")
    }
    

    该方法的思路是通过遍历 typeHolders,通过 ViewHolderCreator 的 isForViewType 方法来判断是否符合条件,如果符合,则在 typeHolders 中取出 viewType 出来返回。

    因为 typeHolders 中的 viewType 是自增的,所以 getItemViewType 的返回值会是 0,1,2,3...

    isForViewType 在实际中如何实现?

    举个例子:
    如果你的数据源由多个实体类组成,比如:

    private List<Object> data = new ArrayList<>();
    data.add(new User("Marry", 17, R.drawable.icon2, "123456789XX"));
    data.add(new SectionHeader("My Images"));
    data.add(new Image(R.drawable.cover1));
    

    那么在构建 EfficientAdapter 时,泛型传入的自然是 Object,然后在 isForViewType 方法中你可以这样区分类型:

     // 代表这是 User 类型
     public boolean isForViewType(Object data, int position) {
       return data instanceof User;
    }
    
     // 代表这是 SectionHeader 类型
     public boolean isForViewType(Object data, int position) {
       return data instanceof SectionHeader;
    }
    
     // 代表这是 Image 类型
     public boolean isForViewType(Object data, int position) {
       return data instanceof Image;
    }
    

    如果你的数据源只有一个实体类,但是实体类里面有某个字段可以区分类型,你可以这样:

     // 代表这是 User 类型
     public boolean isForViewType(ListInfo data, int position) {
       return data.type = ListInfo.USER
    }
    
     // 代表这是 SectionHeader 类型
     public boolean isForViewType(ListInfo data, int position) {
       return data.type = ListInfo.HEADER
    }
    
     // 代表这是 Image 类型
     public boolean isForViewType(ListInfo data, int position) {
       return data.type = ListInfo.IMAGE
    }
    

    其他情况可以根据具体的情况而定。

    到这里,已经完成 Adapter 的封装了,接下来可以定义一些数据源的增删查改的方法,比如:

    //绑定 RecyclerView
    fun attach(recyclerView: RecyclerView) = apply { recyclerView.adapter = this }
    
    //提交数据
    fun submitList(list: MutableList<T>) {
        this.items?.clear()
        this.items?.addAll(list)
        notifyDataSetChanged()
    }
    

    到这里,已经可以简单粗暴的使用了:

    adapter = EfficientAdapter<SectionHeader>()
            .register(object : ViewHolderCreator<SectionHeader>() {
                override fun isForViewType(data: SectionHeader?, position: Int) = data != null
                override fun getResourceId() = R.layout.item_setion_header
    
                override fun onBindViewHolder(
                        data: SectionHeader?,
                        items: MutableList<SectionHeader>?, position: Int,
                        holder: ViewHolderCreator<SectionHeader>
                ) {
                    setText(R.id.section_title, data.title)
                }
            }).attach(recycle_view)
    adapter?.submitList(data)
    

    但和使用效果差别有点大啊。所以,接下来就是 kotlin 发挥的时候了。

    扩展函数 与 DSL

    相信学过 kotlin 的都知道这两个东西,他们可以为我们的代码提供更多的可能。

    ViewHolderCreator DSL

    由于 ViewHolderCreator 是一个抽象类,对它进行 DSL 封装需要一个默认的实现类(也许可以直接封装,但是我只能想到这种方法):

    class ViewHolderDsl<T>(private val resourceId: Int) : ViewHolderCreator<T>() {
        private var viewType: ((data: T?, position: Int) -> Boolean)? = null
        private var viewHolder: ((data: T?, position: Int, holder: ViewHolderCreator<T>) -> Unit)? = null
    
        fun isForViewType(viewType: (data: T?, position: Int) -> Boolean) {
            this.viewType = viewType
        }
    
        fun bindViewHolder(holder: (data: T?, position: Int, holder: ViewHolderCreator<T>) -> Unit) {
            viewHolder = holder
        }
    
        override fun isForViewType(data: T?, position: Int): Boolean {
            return viewType?.invoke(data) ?: (data != null)
        }
    
        override fun getResourceId() = resourceId
    
        override fun onBindViewHolder(
                data: T?, items: MutableList<T>?, position: Int, holder: ViewHolderCreator<T>,
                payloads: MutableList<Any>
        ) {
            viewHolder?.invoke(data, position, holder)
        }
    }
    

    代码比较清晰,就是对三个抽象方法的实现。由于 getResourceId 比较简单,所以直接放在构造方法中传值就好。

    实现好了 ViewHolderDsl,我们给 EfficientAdapter 定义一个扩展函数,用 DSL 的方式去调用 register 方法:

    fun <T : Any> EfficientAdapter<T>.addItem(resourceId: Int, init: ViewHolderDsl<T>.() -> Unit) {
        val holder = ViewHolderDsl<T>(resourceId)
        holder.init()
        register(holder)
    }
    

    比较简单,就是创建好 ViewHolderDsl 后,调用 register 方法即可。

    到这里,其实已经可以用了,只要我们再写一个函数,用 DSL 的方式创建 Adapter 即可:

    fun <T : Any> efficientAdapter(init: EfficientAdapter<T>.() -> Unit): EfficientAdapter<T> {
        val adapter = EfficientAdapter<T>()
        adapter.init()
        return adapter
    }
    

    所以上面那个简单粗暴的示例代码就可以变成这样:

    adapter = efficientAdapter<Any> {
        addItem(R.layout.item_setion_header) {
            isForViewType { it != null }
            bindViewHolder { data, _, _ ->
                setText(R.id.section_title, data.title)
            }
        }
    }.attach(recycle_view)
    adapter?.submitList(data)
    

    代码又清晰和简单了很多。由于在 ViewHolderDsl 中,isForViewType 的默认实现是 data!=null,所以如果是单类型列表,这个方法可以直接不写。

    虽然代码简单了很多,但这样总要定义 adapter 对象和绑定 RecycleView,所以更加优雅的方式就是给 RecycleView 定义一个扩展函数,把这些操作都包装起来。

    首先我们实现一个叫 RecycleSetup 的类,在这个类里面,把 RecycleView 的配置以及 Adapter 操作,数据源操作等通通包装起来:

    class RecycleSetup<T> internal constructor(private val recyclerView: RecyclerView) {
    
        var items = mutableListOf<T>()
        var adapter: EfficientAdapter<T>? = null
        var context = recyclerView.context
    
        fun dataSource(items: MutableList<T>) {
            this.items.clear()
            this.items = items
        }
    
        fun withLayoutManager(init: RecycleSetup<T>.() -> RecyclerView.LayoutManager) =
                apply { recyclerView.layoutManager = init() }
    
        fun adapter(init: EfficientAdapter<T>.() -> Unit) {
            this.adapter = EfficientAdapter()
            init.invoke(adapter!!)
            recyclerView.adapter = adapter
            adapter?.submitList(this.items)
        }
    
        fun submitList(list: MutableList<T>) {
            this.items.clear()
            this.items = list
            adapter?.submitList(this.items)
        }
    
        fun getItem(position: Int): T = items[position]
    }
    

    代码简单,相信大家都能看懂。

    有了这个类,最后,就可以给 RecycleView 实现扩展函数了:

    fun <T> RecyclerView.setup(block: RecycleSetup<T>.() -> Unit): RecycleSetup<T> {
        val setup = RecycleSetup<T>(this).apply(block)
        if (layoutManager == null) {
            layoutManager = LinearLayoutManager(context)
        }
        return setup
    }
    
    fun <T> RecyclerView.submitList(items: MutableList<T>) {
        if (adapter != null && adapter is EfficientAdapter<*>) {
            (adapter as EfficientAdapter<T>).submitList(items)
        }
    }
    

    layoutManager 为空就默认实现 LinearLayoutManager。最后,上面那个简单粗暴的代码就可以写成跟一开始说那个效果一样了:

    recycleView.setup<SectionHeader> {
        adapter {
            addItem(R.layout.item_setion_header) {
                bindViewHolder { data, _, _ ->
                    setText(R.id.section_title, data.title)
                }
            }
        }
    }
    recycleView.submitList(data)
    

    完整代码和例子都在这里 EfficientAdapter ,有兴趣可以看看。

    总结

    其实,整篇文章的代码思路都比较简单,其中比较有意思的是 viewType 自加一这里,在使用的时候用户只需要实现 isForViewType 即可,这可以避免了你的实体类需要继承某一个 Base 类。

    当然相比各个大佬们的库,这个算是比较简单的,所以写这篇文章的原因是分享自己在封装代码的时候的一个思路,一步一步从零到有。相信很多人都需要这种东西,比整天搬砖有意思,也会学到一点点知识。

    Thank you for 看完整篇文章,完!

    相关文章

      网友评论

        本文标题:是时候提高你撸RecycleView的效率了

        本文链接:https://www.haomeiwen.com/subject/qffecktx.html