从零开始:优化代码之MultiTypeAdapter_imkarl.的博客-程序员宅基地

技术标签: 从零开始  优化  MultiTypeAdapter  Android  

引言

Android 开发中经常会需要显示列表,目前 Google 官方推荐的显示方案是:RecyclerView + Adapter
RecyclerView 并没有太复杂的属性配置;所以,要实现列表的展示及相关逻辑,我们主要的工作内容是实现自己的 Adapter。

开始优化

下面我们就以一个类似朋友圈的列表,写一个常见的多类型 Adapter 实现:


class MyAdapter(private val datas: List<Any>): Adapter<ViewHolder>() {
    
    companion object {
    
        const val TYPE_TEXT = 1
        const val TYPE_IMAGE = 2
        const val TYPE_AD = 100
    }

    override fun getItemCount(): Int = datas.size

    override fun getItemViewType(position: Int): Int {
    
        val data = datas[position]
        if (data is PostMsg) {
    
            return data.type
        } else if (data is AdMsg) {
    
            return TYPE_AD
        } else {
    
            throw IllegalArgumentException("Unkown data: $data")
        }
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
    
        val inflater = LayoutInflater.from(parent.context)
        if (viewType == TYPE_TEXT) {
    
            return PostTextViewHolder(inflater.inflate(R.layout.item_left, parent, false))
        } else if (viewType == TYPE_IMAGE) {
    
            return PostImageViewHolder(inflater.inflate(R.layout.item_left, parent, false))
        } else if (viewType == TYPE_AD) {
    
            return AdViewHolder(inflater.inflate(R.layout.item_right, parent, false))
        } else {
    
            throw IllegalArgumentException("Unkown viewType: $viewType")
        }
    }

    override fun onBindViewHolder(holder: ViewHolder, position: Int) {
    
        if (holder is PostTextViewHolder) {
    
            val postMsg = datas[position] as PostMsg
            holder.tvName.text = "postion: $position  ${
      postMsg.label}"
            holder.tvName.setOnClickListener {
    
                // tvName的点击事件
            }
        } else if (holder is PostImageViewHolder) {
    
            val postMsg = datas[position] as PostMsg
            holder.tvName.text = "postion: $position  ${
      postMsg.label}"
            holder.tvName.setOnClickListener {
    
                // tvName的点击事件
            }
        } else if (holder is AdViewHolder) {
    
            val adMsg = datas[position] as AdMsg
            holder.tvName.text = "postion: $position  ${
      adMsg.label}"
            holder.tvName.setOnClickListener {
    
                // tvName的点击事件
            }
        }
    }

    internal class PostTextViewHolder(itemView: View) : ViewHolder(itemView) {
    
        var tvName: TextView = itemView.findViewById(R.id.tvName)
    }
    internal class PostImageViewHolder(itemView: View) : ViewHolder(itemView) {
    
        var tvName: TextView = itemView.findViewById(R.id.tvName)
    }
    internal class AdViewHolder(itemView: View) : ViewHolder(itemView) {
    
        var tvName: TextView = itemView.findViewById(R.id.tvName)
    }
}

事实上,我们真实业务中遇到的,可能要比上面这个例子的类型更多、逻辑更复杂。而且随着需求更迭,类型随时可能增减。维护起来非常不方便。

我期望的结果是:不需要每次都实现一个新的 Adapter,我只需要关系有多少种 ViewHolder,以及每种 ViewHolder 对应的业务逻辑。

下面我们就开始来优化我们的代码,把上面那些机械的、麻烦的逻辑统一处理,让我们只需要写自己真正的业务逻辑就好了。

第一步:优化 onBindViewHolder()

这里每增加一种类型,都要多写一个 if 判断条件,重复而且麻烦,期望能省去这里的类型判断。

我的解决方案是:让 ViewHolder 统一提供 onBindData() 方法,在内部实现自己的逻辑;这样就不需要在 onBindViewHolder() 去判断是哪个 ViewHolder 了。

改造结果如下:


override fun onBindViewHolder(holder: ViewHolder, position: Int) {
    
    if (holder is BasicViewHolder) {
    
        // 这里统一交由 ViewHolder 内部去处理相关逻辑
        (holder as BasicViewHolder<Any>).onBindData(position, datas[position])
    }
}

第二步:优化 onCreateViewHolder()

和 onBindViewHolder() 不同的是,这里只需要根据不同的 type,创建相对应的 ViewHolder 就行了,逻辑单一,并没有复杂的判断。

我提供了一个 ViewHolderFactory 专门用来创建 ViewHolder,并且由外部传入。这样的好处是可以解耦,把独立的逻辑拆分到不同的类中,减少了和具体某个 Adapter 的依赖。
之后我们就可以用相同的 Adapter + 不同的 ViewHolderFactory 组合成列表要展示的数据。

改造结果如下:


override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
    
    val inflater = LayoutInflater.from(parent.context)
    return viewHolderFactory.onCreateViewHolder(inflater, parent, viewType)
}

第三步:优化 getItemViewType()

onCreateViewHolder() 可以根据 type 来创建 ViewHolder,而 type 则是由 getItemViewType() 的返回值来决定的。

很明显,他们有一个承上启下的作用,关系非常紧密。我决定把他一起丢给 ViewHolderFactory 去处理。

并且,我们约定一个 ViewHolder 对应一个布局。所以,getItemViewType() 的返回值取其对应的 layoutResId 即可。

改造结果如下:


override fun getItemViewType(position: Int): Int = viewHolderFactory.getLayoutResId(position, datas[position]!!)

现在,我们将这个通用的 Adapter 命名为:MultiTypeAdapter。

一起来看下它的完整代码:


class MultiTypeAdapter(private val datas: List<Any>,
                       private val viewHolderFactory: BasicViewHolderFactory): Adapter<ViewHolder>() {
    
    override fun getItemCount(): Int = datas.size

    override fun getItemViewType(position: Int): Int = viewHolderFactory.getLayoutResId(position, datas[position])

    override fun onCreateViewHolder(parent: ViewGroup, layoutResId: Int): BasicViewHolder<*> {
    
        val inflater = LayoutInflater.from(parent.context)
        return viewHolderFactory.onCreateViewHolder(inflater, parent, layoutResId)
    }

    override fun onBindViewHolder(holder: ViewHolder, position: Int) {
    
        if (holder is BasicViewHolder<*>) {
    
            // 这里统一交由 ViewHolder 内部去处理相关逻辑
            (holder as BasicViewHolder<Any>).onBindData(position, datas[position])
        }
    }
}

abstract class BasicViewHolderFactory {
    
    @LayoutRes
    abstract fun getLayoutResId(position: Int, data: Any): Int
    abstract fun onCreateViewHolder(inflater: LayoutInflater, parent: ViewGroup, @LayoutRes layoutResId: Int): BasicViewHolder<*>
}

abstract class BasicViewHolder<T>(itemView: View): ViewHolder(itemView) {
    
    abstract fun onBindData(position: Int, data: T)
}

使用时,我们只需要根据需要提供不同的 ViewHolderFactory。

代码如下:


adapter = MultiTypeAdapter(datas, object: BasicViewHolderFactory() {
    
    override fun getLayoutResId(position: Int, data: Any): Int {
    
        return when {
    
            (data is PostMsg && data.isTextPost()) -> R.layout.item_post_text
            (data is PostMsg && data.isImagePost()) -> R.layout.item_post_image
            (data is AdMsg) -> R.layout.item_ad
            else -> throw IllegalArgumentException("Unkown data: $data")
        }
    }

    override fun onCreateViewHolder(inflater: LayoutInflater, parent: ViewGroup, layoutResId: Int): BasicViewHolder<*> {
    
        val itemView = inflater.inflate(layoutResId, parent, false)
        return when(layoutResId) {
    
            R.layout.item_post_text -> PostTextViewHolder(itemView)
            R.layout.item_post_image -> PostImageViewHolder(itemView)
            R.layout.item_ad -> AdViewHolder(itemView)
            else -> throw IllegalArgumentException("Unkown viewType: $layoutResId")
        }
    }
})

之后在每个 ViewHolder 实现自己的业务逻辑。

例如:


class PostTextViewHolder(itemView: View) : BasicViewHolder<PostMsg>(itemView) {
    
    var tvName: TextView = itemView.findViewById(R.id.tvName)

    override fun onBindData(position: Int, data: PostMsg) {
    
        tvName.text = "postion: $position  ${
      data.label}"
        tvName.setOnClickListener {
    
            // tvName的点击事件
        }
    }
}

到此为止,我们得到逻辑简单,并且可以复用的 Adapter。每个 ViewHolder 也比较简单,只需要关心自己的业务逻辑。
但是 ViewHolderFactory 的 getLayoutResId() 和 onCreateViewHolder() 还是存在相似的类型判断。是否还可以再继续优化下呢?

高级优化

前方高能预警!!!请做好准备

getLayoutResId() 用于区分类型,这里的类型判断是没办法省去的。
onCreateViewHolder() 则是一一对应关系,只需要根据提供的 layoutResId 创建对应的 ViewHolder 就可以了。
所以我们的目标就是尽量省去 onCreateViewHolder() 的类型判断。

onCreateViewHolder() 需要2个数据来创建 ViewHolder 实例:layoutResId 以及 目标ViewHolder的类型。
如果在 getLayoutResId() 同时返回 layoutResId、viewHolderClass,岂不是就搞定了?

说干就干,下面先定义一个实体类:ViewHolderType


data class ViewHolderType(
	val clazz: KClass<out BasicViewHolder<*>>,
	@LayoutRes val layoutResId: Int
)

接着改造 BasicViewHolderFactory

  • getLayoutResId() 的返回值改成 ViewHolderType;
  • 去掉 onCreateViewHolder() 方法。

abstract class BasicViewHolderFactory {
    
    abstract fun getViewHolderType(position: Int, data: Any): ViewHolderType
}

最后,修改 MultiTypeAdapter 的实现:

  • getItemViewType() 根据 ViewHolderType 自动生成 viewType;
  • onCreateViewHolder() 再通过 viewType 找到对应的 ViewHolderType,并创建 ViewHolder 实例。

private val holderTypes by lazy {
     mutableMapOf<ViewHolderType, Int>() }
private val viewTypeGenerator by lazy {
     AtomicInteger(1) }

override fun getItemViewType(position: Int): Int {
    
    val holderType = viewHolderFactory.getViewHolderType(position, datas[position])
    if (holderTypes.containsKey(holderType)) {
    
        return holderTypes[holderType]!!
    }

    val viewType = viewTypeGenerator.getAndIncrement()
    holderTypes[holderType] = viewType
    return viewType
}

override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BasicViewHolder<*> {
    
    holderTypes.forEach {
     (_holderType, _viewType) ->
        if (_viewType == viewType) {
    
            val itemView = LayoutInflater.from(parent.context).inflate(_holderType.layoutResId, parent, false)
            return _holderType.clazz.newInstance(itemView)
        }
    }
    throw IllegalArgumentException("Unkown viewType: $viewType")
}

示例

最终使用时,只需要写一次类型判断即可:


adapter = MultiTypeAdapter(datas, object: BasicViewHolderFactory() {
    
    override fun getViewHolderType(position: Int, data: Any): ViewHolderType {
    
        return when {
    
            (data is PostMsg && data.isTextPost()) -> ViewHolderType(PostTextViewHolder::class, R.layout.item_post_text)
            (data is PostMsg && data.isImagePost()) -> ViewHolderType(PostImageViewHolder::class, R.layout.item_post_image)
            (data is AdMsg) -> ViewHolderType(AdViewHolder::class, R.layout.item_ad)
            else -> throw IllegalArgumentException("Unkown data: $data")
        }
    }
})

< 完结 >

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/AlaFighting/article/details/100228386

智能推荐

程序员,建立你的商业意识_weixin_30469895的博客-程序员宅基地

原文在这:http://blog.donews.com/yanhui/archive/2006/03/09/760873.aspx1.1 程序员为什么需要商业意识几年前,当我刚刚认识Fishman的时候,听到他神奇的创业经历,觉得非常不可思议。甚至还专门写了一篇报道发到《电脑报》上,题目是《从程序员到CEO》。不久,Fishman将创建的又一个新公司天...

docker + nginx + uwsgi + ubuntu部署django项目_花开花落与云卷云舒的博客-程序员宅基地

前提:你项目部署的服务器端口是开启的,在外部是可以访问到的。这里不懂就百度一下。并且在服务器上已经安装了docker。一、拉取镜像1.1 拉取python镜像在这里我们拉取的是python的镜像,这样方便我们项目中库包的下载,我们就不用自己取安装pip。python中有自带的pip。sudo docker pull python1.2 制作python容器我们用python镜像制作python容器,hengda是我们的项目名sudo docker run -it --name

unity标准Shader之十种贴图类型_oldboy666的博客-程序员宅基地

                                                              十种贴图类型介绍:标准 Shader 贴图标准 Shader 使用的是 PBR 渲染,基于现实物理效果的渲染表现形式。一个模型能不能使用标准 Shader 来进行渲染,是在做这个模型的贴图的时候决定的。有没有按照 PBR 贴图的制作规范和模式来制作,决定了该模型是否...

5个层级带你看清一颗芯片的内部结构_人工智能学家的博客-程序员宅基地

来源:北京物联网智能技术应用协会导 读在我们阐明半导体芯片之前,我们先应该了解两点。其一半导体是什么,其二芯片是什么。半导体半导体( semiconductor),指常温下导电性能介于...

泛函编程(15)-泛函状态-随意数产生器_weixin_30498921的博客-程序员宅基地

对于OOP程序员来说,泛函状态变迁(functional state transition)是一个陌生的课题。泛函状态变迁是通过泛函状态数据类型(functional state)来实现的。State是一个出现在泛函编程里的类型(type)。与其它数据类型一样,State同样需要自身的一套泛函操作函数和组合函数(combinators),我们将在以下章节中讨论有关State数据类型的设计...

随便推点

python 类实例化,修改属性值_weixin_30474613的博客-程序员宅基地

class User(object): def __init__(self, first_name, last_name, login_attempts): self.first_name = first_name self.last_name = last_name self.login_attempts = login_attempts def incre...

第 9 章 数据管理 - 074 - 如何安装和配置 Rex-Ray?_weixin_33966095的博客-程序员宅基地

Rex-Ray 安装和配置方法Rex-Ray 是一个优秀的 Docker volume driver安装和配置方法Rex-Ray 以 standalone 进程的方式运行在 Docker 主机上,安装方法很简单,在需要使用 Rex-Ray driver 的主机 host1 和 host2上运行如下命令:curl -sSL https://rexray.io/instal...

深入理解卷积神经网络(CNN)——从原理到实现_Rainbow0210的博客-程序员宅基地

王琦      QQ:451165431      计算机视觉&深度学习转载请注明出处 :http://blog.csdn.net/Rainbow0210/article/details/78562926。本篇通过在MNIST上的实验,引出卷积神经网络相关问题,详细阐释其原理以及常用技巧、调参方法。欢迎讨论相关技术&学术问题,但谢绝拿来主义。代码是博主自己写的,因为倾向于详细阐述底层原理,所

(前端)angular报错日常以及记录日常_weixin_30627341的博客-程序员宅基地

目录1json的key...value问题遍历json的key...value的时候报错:[tslint] for (... in ...) statements must be filtered with an if statement  原代码:for (let key in this.targetList[0]) { this.tabl...

Vue教程学习二:_weixin_30471561的博客-程序员宅基地

Vue教程基础中计算属性:关于计算属性是用于解决在view(UI)层使用繁杂的计算或者拼接的方法,将需要的值通过计算属性返回值来展示1.计算属性的返回值放入了缓存:在computed 中定义一个计算属性,计算属性的的返回值放将到缓存中(computed 是基于它的依赖缓存,只有相关依赖发生改变时才会重新取值),比如在computed中有个gettiem() 函数,它的返回值是re...

【MM S4】合作伙伴_weixin_30466953的博客-程序员宅基地

SAPS/4HANA现在把供应商、客户、人员都叫做商业伙伴(BusinessPartner),通过事物码(T-code:BP)来操作,之前XD01/XD02/XD03,或者VD01/VD02/VD03,XK01/XK02/XK0,或者MK01/MK02/MK03等都不能使用了。如果你使用这些事物码,SAP跳转到BP界面以下为转载,原文地址:https://blogs.sa...