美文网首页Android进阶之路Android开发
代理模式应用 | 每当为 RecyclerView 新增类型时就

代理模式应用 | 每当为 RecyclerView 新增类型时就

作者: 木木玩Android | 来源:发表于2020-09-29 21:35 被阅读0次

    App 界面愈发复杂,元素越来越多,就算手机屏幕再大也无法在一屏中展示所有内容。将不同类型的元素组织成 RecyclerView 就可以超越屏幕的限制。常用的RecyclerView在使用时有诸多痛点,比如“如何处理表项及其子控件点击事件?”、“如何在为列表新增类型时不抓狂?”、“如何低成本地刷新列表?”、“如何预加载下一屏数据?”等等。这一篇尝试让扩展列表数据类型变得简单。

    单类型列表

    项目刚开始时,新闻列表还不复杂,就是单纯的Feed流,左边图片,右边标题的那种,对应的Adapter实现也一样简单:

    // 新闻适配器
    class NewsAdapter : RecyclerView.Adapter<NewsViewHolder>() {
        // 新闻列表
        var news: List<News>? = null
            set(value) {
                field = value
                notifyDataSetChanged()
            }
    
        override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): NewsViewHolder {
            // 构建新闻列表表项
            val itemView = ...//省略构建细节
            return NewsViewHolder(itemView)
        }
    
        override fun getItemCount(): Int {
            return news?.size ?: 0
        }
    
        override fun onBindViewHolder(holder: NewsViewHolder, position: Int) {
            news?.getOrNull(position)?.let { holder.bind(it) }
        }
    }
    
    // 新闻ViewHolder
    class NewsViewHolder(itemView: View):RecyclerView.ViewHolder(itemView) {
        fun bind(news:News){
            // 将新闻填充入新闻表项视图
            itemView.apply {... // 省略绑定细节}
        }
    }
    
    // 新闻实体类
    data class News(
        @SerializedName("image") var image: String?,
        @SerializedName("title") var title: String?
    )
    复制代码
    
    1. 定义实体类
    2. 定义与实体类绑定的ViewHolder
    3. 定义与ViewHolder绑定的Adapter

    构建 RecyclerView 时,大多遵循这样的步骤。

    带空视图的列表

    展示网络内容的列表通常会带有一个空视图,以在网络请求失败时提醒用户。

    刚才定义的NewsAdapter已经和NewsViewHolder耦合,不能满足新增列表空视图的需求,重构如下:

    // 列表适配器基类
    abstract class BaseRecyclerViewAdapter<T> : RecyclerView.Adapter<BaseViewHolder?> {
        companion object {
            const val TYPE_EMPTY_VIEW = -1 // 空视图
            const val TYPE_CONTENT = -2 // 非空视图
        }
        // 列表数据
        protected var datas: MutableList<T>? = null
        // 空视图
        private var emptyView:View? = null
        // 当前列表状态(空或非空)
        private var currentType = 0
        // 监听列表数据变化,及时更新列表是否是空状态
        private inner class DataObserver : RecyclerView.AdapterDataObserver() {
            override fun onChanged() {
                // 根据列表数据长度更新当前列表状态
                currentType = if (datas != null && datas.size != 0) {
                    TYPE_CONTENT
                } else {
                    TYPE_EMPTY_VIEW
                }
            }
        }
        // 供子类实现以定义如何构建表项
        protected abstract fun createHolder(parent: ViewGroup?, viewType: Int, inflater: LayoutInflater?): BaseViewHolder
        // 供子类实现以定义如何将数据绑定到表项视图
        protected abstract fun bindHolder(holder: BaseViewHolder?, position: Int)
        // 供子类定义真实表项长度
        protected abstract val count: Int
        // 供子类定义真实表项类型
        protected abstract fun getViewType(position: Int): Int
    
        constructor(context: Context) {
            this.context = context
            // 监听列表数据变化
            registerAdapterDataObserver(DataObserver())
        }
    
        fun setData(datas: MutableList<T>?) {
            this.datas = datas
            notifyDataSetChanged()
        }
        // 注入空视图
        fun setEmptyView(emptyView: View) {
            this.emptyView = emptyView
        }
        // 创建表项视图
        override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BaseViewHolder {
            val viewHolder: BaseViewHolder
            when (viewType) {
                TYPE_EMPTY_VIEW -> { viewHolder = BaseViewHolder(emptyView) }
                // 非空表项构建逻辑延迟到子类实现
                else -> viewHolder = createHolder(parent, viewType)
            }
            return viewHolder
        }
        // 为表项视图绑定数据
        override fun onBindViewHolder(holder: BaseViewHolder?, position: Int) {
            val viewType = getItemViewType(position)
            when (viewType) {
                // 空视图不需要绑定
                TYPE_EMPTY_VIEW -> {}
                // 非空视图绑定逻辑延迟到子类实现
                else -> bindHolder(holder, position)
            }
        }
    
        override fun getItemCount(): Int {
            // 如果列表为空,则列表长度为1(听着就很别扭),否则返回列表实际长度
            return if (currentType == TYPE_EMPTY_VIEW) { 1 } else count
        }
    
        override fun getItemViewType(position: Int): Int {
            // 如果列表为空,则返回空列表类型,否则返回真实列表表项类型
            return if (datas == null || datas.size == 0) {
                TYPE_EMPTY_VIEW
            } else {
                getViewType(position)
            }
        }
    }
    
    // ViewHolder基类
    public class BaseViewHolder extends RecyclerView.ViewHolder {
        public BaseViewHolder(View itemView) { super(itemView); }
    }
    复制代码
    

    抽象了一个基类BaseAdapter,它分别在onCreateViewHolder()onBindViewHolder()getItemCount()getItemViewType()这四个方法中通过if-else增加了空视图逻辑分支,子类需要实现与之对应的四个抽象方法以定义业务表项,并可以通过setEmptyView()将空视图注入。

    BaseAdapter还引入了ViewHolder基类,使得构造 Adapter 时不再需要和一个具体的ViewHolder绑定,这为扩展列表数据类型提供了方便。

    但这个方案有一点“拧巴”,从getItemCount()的实现就可以看出:

    override fun getItemCount(): Int {
        // 如果列表为空,则列表长度为1(听着就很别扭),否则返回列表实际长度
        return if (currentType == TYPE_EMPTY_VIEW) { 1 } else count
    }
    复制代码
    

    拧巴的点在于:明明空视图也是列表的一种表项,但却把对它的处理“特殊化”。而且通过if-else实现的特殊化处理是没有扩展性的!假设需要为列表新增 header 和 footer,那还得修改基类BaseAdapter,为其增加更多的if-else分支,需求一变就得修改基类,那这个基类着实有点“鸡肋”。

    伪多类型列表

    随着版本的迭代,需要在列表顶部插入 Banner,并和新闻一起滚动。

    虽然BaseAdapter对扩展不太友好,但还能凑合着用(毕竟它处理了空视图逻辑)。这一次的新需求可以通过在子类中新增if-else来扩展:

    // 新闻适配器
    class NewsAdapter : BaseRecyclerViewAdapter<News>() {
        val TYPE_NEWS = 1
        val TYPE_BANNER = 2
        // 通过 if-else 为列表新增类型
        override fun createHolder(parent: ViewGroup, viewType: Int): BaseViewHolder {
            if (viewType == TYPE_NEWS){
                return createNewsViewHolder(parent)
            }else {
                return createBannerViewHolder(parent)
            }
        }
        // 通过 if-else 为不同表项绑定数据
        override fun bindHolder(holder: BaseViewHolder, position: Int) {
            val viewType = getViewType(position)
            if (viewType == TYPE_NEWS){
                (holder as? NewsViewHolder)?.bind(datas[position])
            }else {
                (holder as? BannerViewHolder)?.bind(datas[position])
            }
        }
    
        override val count: Int
            protected get() = if (datas == null) 0 else datas.size
    
        // 构建 Banner 表项
        private fun createBannerViewHolder(parent: ViewGroup): BaseViewHolder {
            val itemView = ... // 省略构建细节
            return BannerViewHolder(itemView)
        }
    
        // 构建新闻表项
        private fun createNewsViewHolder(parent: ViewGroup): BaseViewHolder {
            val itemView = ... // 省略构建细节
            return NewsViewHolder(itemView)
        }
    }
    
    // 新闻ViewHolder
    class NewsViewHolder(itemView: View): BaseViewHolder(itemView) {
        fun bind(news:News){ ... // 省略绑定细节 }
    }
    
    // Banner ViewHolder
    class BannerViewHolder(itemView: View): BaseViewHolder(itemView) {
        fun bind(news:News){ ...// 省略绑定细节 }
    }
    
    // 新闻实体类(新增了banner字段)
    data class News(
        @SerializedName("image") var image: String?,
        @SerializedName("title") var title: String?,
        var banners: List<Banner>?
    )
    
    // Banner实体类(从有别于新闻接口的另一个接口返回)
    data class Banner(
        @SerializedName("jumpUrl") var jumpUrl: String?,
        @SerializedName("imageUrl") var imageUrl: String?
    )
    复制代码
    

    如果项目中的RecyclerView是这样的话,每次为它新增类型之时,即是你抓狂之时。

    • 因为NewsAdapter和具体的News实体类耦合,所以新增的数据类型只能是News的成员,虽然从业务上讲,列表新增了一种新类型,但代码中将两种类型揉成了一种(伪多类型)。新增 n 个类型,News类就会增加 n 个成员,而且每一种表项只会使用到其中的某个字段,其余字段对它来说都是冗余。

    • 因为不同类型表项是通过if-else来区别,所以每新增一个类型,就得修改NewsAdapter类。对既有类的修改是危险的,因为它可能已经被多人补丁而变得“坑坑洼洼”,到处是不可言喻的“潜规则”,只要一不留心就可能“引爆地雷”。

    抓狂只是前戏,莫名其妙的 bug 蜂拥而至才是高潮。细心的你一定发现了,通过这种方式为列表新增类型会破坏基类中空视图的逻辑:

    abstract class BaseRecyclerViewAdapter<T> : RecyclerView.Adapter<BaseViewHolder?> {
        // 监听列表数据变化,及时更新列表是否是空状态
        private inner class DataObserver : RecyclerView.AdapterDataObserver() {
            override fun onChanged() {
                // 如果列表没有数据,则加载空视图,否则加载业务数据
                currentType = if (datas != null && datas.size != 0) {
                    TYPE_CONTENT
                } else {
                    TYPE_EMPTY_VIEW
                }
            }
        }
    }
    复制代码
    

    当 banner 接口返回数据而新闻接口返回空时,此时列表长度不为 0,所以BaseAdapter不会展示空视图。产品期望没有新闻时,新闻区域的空视图,当 banner和新闻都没有时,展示整个列表的空视图。继续重构基类?(真想删掉这个基类)

    类型无关适配器

    现有Adapter难扩展,一扩展就出 bug 的原因是适配器和具体数据类型耦合

    有没有可能设计一种和具体类型无关的适配器?就像这样:

    class VarietyAdapter(
        var dataList: MutableList<Any> = mutableListOf()
    ) : RecyclerView.Adapter<ViewHolder>() { }
    
    // 构建一个包含三种数据类型的列表,它们分别展示一条新闻、一个Banner,一张图片
    val adapter = VarietyAdapter().apply {
        dataList = mutableListOf (
            News(),
            Banner(),
            "https:xxx"
        )
    }
    复制代码
    

    VarietyAdapter的声明避开了所有和类型相关的信息:

    • 原本RecyclerView.Adapter的子类必须声明一个具体的ViewHolder类型,这里直接使用了RecyclerView.ViewHolder基类。
    • 原本RecyclerView.Adapter中的datas必须是一个存放具体数据类型的列表,这里直接使用了所有非空类型的基类Any

    VarietyAdapter添加数据的时候,将不同类型的数据揉搓在一个列表中。

    一个新的Adapter通常用实现下面这三个方法:

    class VarietyAdapter(
        var datas: MutableList<Any> = mutableListOf()
    ):RecyclerView.Adapter<RecyclerView.ViewHolder>() {
        // 构建表项布局
        override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {}
        // 填充表项内容
        override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {}
        // 获取表项数量
        override fun getItemCount(): Int {}
    }
    复制代码
    

    其中onCreateViewHolder()onBindViewHolder()的实现会和表项的数据类型绑定。

    构建表项填充表项这两个抽象的动作,不会随着业务变化而变化的,但构建什么表项怎么填充表项是两个具体的动作,会随着业务的变化而变化。

    之前犯的错误就是由Adapter亲自处理“具体动作”,导致难以扩展,并且会使Adapter随业务的变化而变。

    是不是可以把“具体动作”抽离出Adapter,交由其他角色处理,而Adapter只和抽象打交道?这样就可以改进Adapter的扩展性。

    // Adapter 代理
    abstract class Proxy<T, VH : RecyclerView.ViewHolder> {
        // 构建表项
        abstract fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder
        // 填充表项
        abstract fun onBindViewHolder(holder: VH, data: T, index: Int, action: ((Any?) -> Unit)? = null)
        // 填充表项(局部刷新)
        open fun onBindViewHolder(holder: VH, data: T, index: Int, action: ((Any?) -> Unit)? = null, payloads: MutableList<Any>) {
            onBindViewHolder(holder, data, index, action)
        }
    }
    复制代码
    

    声明一个代理类,它看上去和RecyclerView.Adapter没什么两样,几乎拥有相同的接口,目的是为了把原本RecyclerView.Adapter做的事情,由它来代理。

    代理类定义了两个类型参数,第一个T表示表项对应数据的类型,第二个VH表示表项ViewHolder的类型。

    代理类是抽象的,每一个它的实例代表着一种类型的表项,并和一种数据类型对应。

    一个代理类的实例通常长这个样子:

    // 文字类表项代理(对应的数据是Text,对应的ViewHolder是TextViewHolder)
    class TextProxy : Proxy<Text, TextViewHolder>() {
        override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
            // 构建表项视图(构建布局 DSL)
            val itemView = parent.context.run {
                TextView {
                    layout_id = "tvName"
                    layout_width = wrap_content
                    layout_height = wrap_content
                    textSize = 40f
                    gravity = gravity_center
                    textColor = "#ff00ff"
                }
            }
            // 构建表项ViewHolder
            return TextViewHolder(itemView)
        }
    
        // 绑定表项数据
        override fun onBindViewHolder(holder: TextViewHolder, data: Text, index: Int, action: ((Any?) -> Unit)?) {
            holder.tvName?text = data.text
        }
    }
    
    // 与文字类表项对应的“文字数据”
    data class Text( var text: String )
    
    // 文字类表项ViewHolder
    class TextViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
        val tvName = itemView.find<TextView>("tvName")
    }
    复制代码
    

    构建Proxy时需要指定与表项对应的数据和ViewHolder,把他们定义在同一个 Kotlin 文件中,以方便修改。

    其中构建布局用到的DSL,可以参考这里

    RecyclerView.Adapter会同时持有一组数据和若干代理类的实例,它的作用变为根据数据类型将“构建表项”和“填充表项”的 任务分发给对应的代理类。

    class VarietyAdapter(
        // 代理列表
        private var proxyList: MutableList<Proxy<*, *>> = mutableListOf(),
        // 数据列表
        var dataList: MutableList<Any> = mutableListOf()
    ) : RecyclerView.Adapter<ViewHolder>() {
        // 注入代理
        fun <T, VH : ViewHolder> addProxy(proxy: Proxy<T, VH>) {
            proxyList.add(proxy)
        }
        // 移除代理
        fun <T, VH : ViewHolder> removeProxy(proxy: Proxy<T, VH>) {
            proxyList.remove(proxy)
        }
        // 将构建表项布局分发给代理
        override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
            return proxyList[viewType].onCreateViewHolder(parent, viewType)
        }
        // 将填充表项分发给代理
        override fun onBindViewHolder(holder: ViewHolder, position: Int) {
            (proxyList[getItemViewType(position)] as Proxy<Any, ViewHolder>).onBindViewHolder(holder, dataList[position], position, action)
        }
        // 将填充表项分发给代理(布局刷新)
        override fun onBindViewHolder(holder: ViewHolder, position: Int, payloads: MutableList<Any>) {
            (proxyList[getItemViewType(position)] as Proxy<Any, ViewHolder>).onBindViewHolder( holder, dataList[position], position, action, payloads )
        }
        // 返回数据总量
        override fun getItemCount(): Int = dataList.size
        // 获取表项类型
        override fun getItemViewType(position: Int): Int {
            return getProxyIndex(dataList[position])
        }
        // 获取代理在列表中的索引
        private fun getProxyIndex(data: Any): Int = proxyList.indexOfFirst {
            // 如果Proxy<T,VH>中的第一个类型参数T和数据的类型相同,则返回对应代理的索引
            (it.javaClass.genericSuperclass as ParameterizedType).actualTypeArguments[0].toString() == data.javaClass.toString()
        }
        // 抽象代理类
        abstract class Proxy<T, VH : ViewHolder> {
            abstract fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder
            abstract fun onBindViewHolder(holder: VH, data: T, index: Int, action: ((Any?) -> Unit)? = null)
            open fun onBindViewHolder(holder: VH, data: T, index: Int, action: ((Any?) -> Unit)? = null, payloads: MutableList<Any>) {
                onBindViewHolder(holder, data, index, action)
            }
        }
    }
    复制代码
    

    VarietyAdapter将一组代理存储在ArrayList结构中:var proxyList: MutableList<Proxy<*, *>> = mutableListOf(),其中将声明Proxy必须指定的两个类型做了star投影,目的是为了让任何类型都能成为Proxy<*,*>的子类型。(关于类型投影的详细介绍可以点击这里

    原本返回表项类型的方法getItemViewType(),现在返回的是代理的索引值,这样在构建和绑定表项时就可以通过该索引在proxyList中找到与数据对应的代理并委托之。

    代理的索引值是通过遍历proxyList并将每一个代理和数据类型做比较,比较的内容是“代理的第一个类型参数 和 数据类型 是否一致”,如果一致,则表示该代理是指定数据对应的代理。

    (proxy.javaClass.genericSuperclass as ParameterizedType)// 获取类型参数列表
        .actualTypeArguments[0].toString()// 获取类型参数表列中的第一个类型
    复制代码
    

    对于一个泛型类Proxy<T, VH : ViewHolder>的实例proxy上面的表达式返回的是Proxy的第一个类型参数T的完整类名。

    然后就可以像这样使用VarietyAdapter了:

    // 构建适配器
    val varietyAdapter = VarietyAdapter().apply {
        // 为Adapter添加两种代理,分别显示文字和图片
        addProxy(TextProxy())
        addProxy(ImageProxy())
        // 构建数据(不同数据类型融合在一个列表中)
        dataList = mutableListOf(
            Text("item 1"), // 代表文字表项
            Image("#00ff00"), //代表图片表项
            Text("item 2"),
            Text("item 3"),
            Image("#88ff00")
        )
        notifyDataSetChanged()
    }
    // 将Adapter赋值给RecyclerView
    recyclerView?.adapter = varietyAdapter
    recyclerView?.layoutManager = LinearLayoutManager(this)
    复制代码
    

    其中ImageProxyImage是和上面提到的TextProxyText类似的代理及数据类型。

    单类型多匹配

    有时候服务器返回的列表数据中有type字段,用于指示客户端应该展示哪种布局,即同一个数据类型对应了多种布局方式,VarietyAdapter现有的做法不能满足这个需求,因为匹配规则被写死在getProxyIndex()方法中。

    为了扩展匹配规则,新增接口:

    // 数据和代理的对应关系
    interface DataProxyMap {
        // 将数据转换成代理类名
        fun toProxy(): String
    }
    复制代码
    

    然后让数据类实现这个接口:

    data class Text(
        var text: String,
        var type: Int // 用type指定布局类型
    ) : VarietyAdapter.DataProxyMap {
        override fun toProxy(): String {
            return when (type) {
                1 -> TextProxy1::class.java.toString() // type为1时对应TextProxy1
                2 -> TextProxy2::class.java.toString() // type为2时对应TextProxy2
                else -> TextProxy2::class.java.toString()
            }
        }
    }
    复制代码
    

    还得修改下getProxyIndex()方法:

    private fun getProxyIndex(data: Any): Int = proxyList.indexOfFirst {
        // 获取代理类中第一个类型参数的类名
        val firstTypeParamClassName = (it.javaClass.genericSuperclass as ParameterizedType).actualTypeArguments[0].toString()
        // 获取代理类名
        val proxyClassName = it.javaClass.toString()
        // 首要匹配条件:代理类第一个类型参数和数据类型相同
        firstTypeParamClassName == data.javaClass.toString()
                // 次要匹配条件:数据类自定义匹配代理名和当前代理名相同
                && (data as? DataProxyMap)?.toProxy() ?: proxyClassName == proxyClassName
    }
    复制代码
    

    原文链接:https://juejin.im/post/6876967151975006221

    相关文章

      网友评论

        本文标题:代理模式应用 | 每当为 RecyclerView 新增类型时就

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