经验之谈 - RecyclerView中多ViewType的封装

235 阅读4分钟

再android app开发中,多个ViewType类型的RecyclerView场景还是很常见的。

常规的实现逻辑:把所有类型item的创建、数据绑定等逻辑都放到同一个RecyclerView.Adapter子类中。随着ViewType类型数量的增加,Adapter类的代码量就急剧膨胀。

该如何优化呢?

把每个ViewType对应的逻辑都独立出来是个不错的尝试:

  1. 为每个ViewType准备一个抽象类SubAdapter的子类,该类中也有onCreateViewHolder、onBindViewHolder等抽象方法,然后把本来写在RecyclerView.Adapter中的代码拷贝到SubAdapter对应的方法中来。这样不同ViewType的逻辑都汇聚到各自的SubAdapter中啦。

  2. 把被掏空的RecyclerView.Adapter改造成MultiItemTypeAdapter,该主adapter中有每个SubAdapter子类的唯一实例,当主adapter的onCreateViewHolder、onBindViewHolder等方法执行时,就把逻辑委托给对应的SubAdapter执行。

先来看下使用时的完整流程:

class MultiViewTypeActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_multi_view_type)
        initView()
    }

    private fun initView(){
        recycler_view.layoutManager = LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false)

        val adapter = MultiItemTypeAdapter<ValueBean>(NormalSubAdapter(), HeaderSubAdapter()) //构造函数中注册
        adapter.registerSubAdapter(ADSubAdapter()) //也可以调用方法注册
        adapter.registerSubAdapter(FooterSubAdapter())
        adapter.syncData(genListData())
        recycler_view.adapter = adapter
    }

    //Header布局相关的所有逻辑都封装到这个独立的类中。
    class HeaderSubAdapter: SubAdapter<ValueBean, HeaderSubAdapter.HeaderItemViewHolder>() {
        override fun onCreateViewHolder(parent: ViewGroup): HeaderItemViewHolder {
            val itemView = LayoutInflater.from(parent.context)
                .inflate(R.layout.item_multi_viewtype_header, parent, false)
            return HeaderItemViewHolder(itemView)
        }

        override fun onBindViewHolder(holder: HeaderItemViewHolder, position: Int) {
            holder.contentView.text = "this is header"
        }

        //判断列表中position这个位置是否Header item
        override fun isSameViewType(position: Int): Boolean {
            val listData = getData() //SubAdapter中可以通过getData()方法获取列表数据
            return listData[position] is HeaderWrapper
        }

        //Header布局对应的ViewHolder
        class HeaderItemViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
            val contentView = itemView.findViewById<TextView>(R.id.content_view)
        }

    }

    //Footer布局
    class FooterSubAdapter: SubAdapter<ValueBean, FooterSubAdapter.FooterItemViewHolder>() {
        override fun onCreateViewHolder(parent: ViewGroup): FooterItemViewHolder {
            val itemView = LayoutInflater.from(parent.context)
                .inflate(R.layout.item_multi_viewtype_footer, parent, false)
            return FooterItemViewHolder(itemView)
        }

        override fun onBindViewHolder(holder: FooterItemViewHolder, position: Int) {
            holder.contentView.text = "this is Footer"
        }

        override fun isSameViewType(position: Int): Boolean {
            return getData()[position] is FooterWrapper
        }

        class FooterItemViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
            val contentView = itemView.findViewById<TextView>(R.id.content_view)
        }

    }

    //广告布局
    class ADSubAdapter: SubAdapter<ValueBean, ADSubAdapter.ADItemViewHolder>(){
        override fun onCreateViewHolder(parent: ViewGroup):ADItemViewHolder {
            val itemView = LayoutInflater.from(parent.context)
                .inflate(R.layout.item_multi_viewtype_ad, parent, false)
            return ADItemViewHolder(itemView)
        }

        override fun onBindViewHolder(holder: ADItemViewHolder, position: Int) {
            holder.contentView.text = "this is AD Unit"
        }

        override fun isSameViewType(position: Int): Boolean {
            return getData()[position] is ADWrapper
        }

        class ADItemViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
            val contentView = itemView.findViewById<TextView>(R.id.content_view)
        }

    }

    //常规布局
    //DefaultSubAdapter默认可以匹配任何一个position,所以不需要实现isSameViewType()方法;
    //匹配规则:优先匹配其他SubAdapter,都匹配不上在用这个兜底。
    //通常用于其他ViewType都有详细的匹配规则,但是自己的匹配规则不明确或繁杂(先把其他的都排除,剩下的就是自己的)
    class NormalSubAdapter: DefaultSubAdaper<ValueBean, NormalSubAdapter.NormalItemViewHolder>(){
        override fun onCreateViewHolder(parent: ViewGroup): NormalItemViewHolder {
            val itemView = LayoutInflater.from(parent.context)
                .inflate(R.layout.item_multi_viewtype_layout_normal, parent, false)
            return NormalItemViewHolder(itemView)
        }

        override fun onBindViewHolder(holder: NormalItemViewHolder, position: Int) {
            holder.contentView.text = "this is normal ${getData()[position].content} data"
        }

        class NormalItemViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
            val contentView = itemView.findViewById<TextView>(R.id.content_view)
        }

    }

    //模拟列表数据
    private fun genListData(): List<ValueBean>{
        val result = arrayListOf<ValueBean>()
        result.add(HeaderWrapper())
        result.add(ValueBean("aaa"))
        result.add(ValueBean("bbb"))
        result.add(ValueBean("ccc"))
        result.add(ADWrapper())
        result.add(ValueBean("ddd"))
        result.add(ValueBean("eee"))
        result.add(ValueBean("fff"))
        result.add(ValueBean("ggg"))
        result.add(ValueBean("hhh"))
        result.add(FooterWrapper())
        return result
    }

}

效果如果下:

Screenshot\_20241227\_174033.png

可以看到示例中SubAdapter子类中的方法和RecyclerView.Adapter中并不是严格的一一对应(缺少getItemViewType()方法):

image.png

所以在SubAdapter中只需要实现 isSameViewType() 方法就足够啦。

然而在某些情况下,isSameViewType() 的判断条件并不太好确定,如上述代码示例中,特殊item(header、footer、广告)都有简单的判断条件,反而主要item类型没有明确的判断方法。

这种情况可以继承 DefaultSubAdaper类(继承SubAdapter,它的 isSameViewType() 返回值恒为true);当某个position其他SubAdapter都匹配不上时,则最后使用它来兜底。

使用configSpanSizeLookup()可以简化GridLayoutManager spanSize设置,同时继承SubAdapter时复写getSpanSize()指定item的spansize。

其他建议:

  1. MultiItemTypeAdapter只应用在列表viewType较多,后续可能还会增加的场景。

  2. list列表中的数据都要求数据类型一致;一些特殊Item可以扩展主Item的数据类型(例如示例中的HeaderWrapper、ADWrapper等类)。 这样可以便于统一管理,如果特殊item还需要携带数据,可以在xxxWrapper类中增加data字段携带数据.

  3. 如果不想列表数据保持类型一致,即list<Object>列表,创建adapter时指定Object类型即可(例如:MultiItemTypeAdapter<Any>())。

好啦 经过简单的封装,Adapte类膨胀的问题得到解决,同时也尽量保留了常规方式代码编写流程。

源码代码量较少,感兴趣可以了解:

完整工程:github.com/High-Power-…

MultiItemTypeAdapter: github.com/High-Power-…

SubAdapter: github.com/High-Power-…

DefaultSubAdaper: github.com/High-Power-…

写完博客后才发现已经有类似的封装库 参考:github.com/drakeet/Mul…

如果内容有错误、某些场景下使用不便,或者优化建议,欢迎在评论中指出!!!