[Day5] Android - Kotlin笔记:ListAdapter + DiffUtil 进阶应用 - 复数itemViewType

Problem

昨天我们提到ListAdapter + DiffUtil在一般RecyclerView的基本使用。
而实际上工作中我们经常会需要在RecyclerView上显示不同的itemView
我们会添加FooterHeader,或是不同样式的itemView
这时候该怎麽办呢?


Solution

submitList之前,把dataList中的item顺序重新编排後,
再使用submitList提交我们的dataList做更新。


实作

假设我们要做一个像Line一样的聊天室,
而且底部要添加一个名为“已滑至底”的TextView做为Footer

资料的部分会用Message这个data class来显示讯息,
并根据isFromMe来判断是否为自己发送的讯息,展示不同的`itemView

data class Message(
    val id: Long,
    val timeStamp: Long,
    val isFromMe: Boolean,
    val message: String
)

1. ListAdapter

首先,因为会展示不同的项目,我们不喂ListAdapterdata class了,
改吃自己创建的sealed class - 这边我们命名为DataItem

class MessageAdapter() : ListAdapter<DataItem, RecyclerView.ViewHolder>(DiffCallback()) {}

2. 类别控管

我们添加一个sealed class
这个sealed class负责用来控管不同item的型态类别。
这边我们列举出ItemFooter这两个型态(data type)。

因为DiffUtil需要一个参数作为判断新旧item是否一样的依据。
所以我们创建一个abstract item id作为interface回传判别用的数据。

isFromMe则是用来判断显示讯息在左侧还是右侧的item view

sealed class DataItem {

    abstract val id: Long
    abstract val isFromMe: Boolean

    data class Item(val message: Message) : DataItem() {
        override val id = message.id
        override val isFromMe = message.isFromMe
    }

    object Footer : DataItem() {
        override val id = Long.MIN_VALUE
        override val isFromMe = false
    }
}

(关於sealed class後续文章有机会会讲解,或是你也可以看这篇写得很详尽)

这边因为Footer只是作为静态显示layout
因此只使用object而非data class


3. DiffUtil

DiffUtil也改为判断DataItem中的id

    class DiffCallback : DiffUtil.ItemCallback<DataItem>() {
        override fun areItemsTheSame(oldItem: DataItem, newItem: DataItem): Boolean {
            return oldItem.id == newItem.id
        }

        override fun areContentsTheSame(oldItem: DataItem, newItem: DataItem): Boolean {
            return oldItem == newItem
        }

    }

4. ItemViewType

创建一个enum class列举需展示的所有viewType

    enum class ItemViewType {
        MESSAGE_FROM_ME, MESSAGE_TO_ME, FOOTER
    }

这边列举viewType,是给Adapter判断要展示哪个ViewHolder来使用的。


5. getItemViewType

因为我们有不同的型别需判断,
所以我们要覆写getItemViewType
键盘按下control+o,选择getItemViewType并覆写他。

    override fun getItemViewType(position: Int): Int {
        return when (getItem(position)) {
            is DataItem.FromMe -> ItemViewType.MESSAGE_FROM_ME.ordinal
            is DataItem.ToMe -> ItemViewType.MESSAGE_TO_ME.ordinal
            is DataItem.Footer -> ItemViewType.FOOTER.ordinal
        }
    }

这边返回的int是onCreateViewHolder会用到的viewType,继续往下看下去。


6. 新增onCreateViewHolderonBindViewHolder判断

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
        return when (viewType) {
            ItemViewType.MESSAGE_FROM_ME.ordinal -> FromMeViewHolder.from(parent)
            ItemViewType.MESSAGE_TO_ME.ordinal -> ToMeViewHolder.from(parent)
            else -> FooterViewHolder.from(parent)
        }
    }

    override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
        when (holder) {

            is FromMeViewHolder -> {
                val data = getItem(position) as DataItem.Item
                holder.bind(data.message)
            }

            is ToMeViewHolder -> {
                val data = getItem(position) as DataItem.Item
                holder.bind(data.message)
            }

            is FooterViewHolder -> {
            }
        }
    }

7. 新增对应的ViewHolder

FromMeToMeFooter创建对应的ViewHolder

    class FromMeViewHolder private constructor(val binding: ItemAccountHistoryNextContentBinding) :
        RecyclerView.ViewHolder(binding.root) {

        fun bind(message: Message) {
            itemView.apply {
                tv_message_from_me.text = message.content
            }
        }

        companion object {
            fun from(parent: ViewGroup): RecyclerView.ViewHolder {
                val view = LayoutInflater.from(parent.context).inflate(R.layout.itemview_message_from_me, parent, false)
                return ToMeViewHolder(view)
            }
        }


    }

    class ToMeViewHolder (view: View) : RecyclerView.ViewHolder(view) {

        fun bind(message: Message) {
            itemView.apply {
                tv_message_to_me.text = message.content
            }
        }

        companion object {
            fun from(parent: ViewGroup): RecyclerView.ViewHolder {
                val view = LayoutInflater.from(parent.context).inflate(R.layout.itemview_message_to_me, parent, false)
                return ToMeViewHolder(view)
            }
        }

    }

    class FooterViewHolder (view: View) : RecyclerView.ViewHolder(view) {
        companion object {
            fun from(parent: ViewGroup): RecyclerView.ViewHolder {
                val view = LayoutInflater.from(parent.context).inflate(R.layout.itemview_footer, parent, false)
                return FooterViewHolder(view)
            }
        }
    }

8. submitList

最後一步了!
现在因为ListAdapter吃的是DataItem,
我们只要创建一个function - addFooterAndSubmitList来取代原本的submitList
利用DataItem整理过後再交给submitList处理就大功告成。

    private val adapterScope = CoroutineScope(Dispatchers.Default)

    fun addFooterAndSubmitList(list: List<Message>) {
        adapterScope.launch {
            val items = list.map { 
                if (it.isFromMe) DataItem.FromMe(it)
                else DataItem.ToMe(it)
            } + listOf(DataItem.Footer)
            withContext(Dispatchers.Main) { //update in main ui thread
                submitList(items)
            }
        }
    }

只要透过呼叫addFooterAndSubmitList就能让ListAdapter成功运作,
DiffUtil自动去筛选判断更新的内容。

    rvAdapter.addFooterAndSubmitList(dataList)

全程序码展示:

  • Adapter

data class Message(
    val id: Long,
    val timeStamp: Long,
    val isFromMe: Boolean,
    val content: String
)

class MessageAdapter() : ListAdapter<DataItem, RecyclerView.ViewHolder>(DiffCallback()) {

    enum class ItemViewType {
        MESSAGE_FROM_ME, MESSAGE_TO_ME, FOOTER
    }

    private val adapterScope = CoroutineScope(Dispatchers.Default)

    fun addFooterAndSubmitList(list: List<Message>) {
        adapterScope.launch {
            val items = list.map {
                if (it.isFromMe) DataItem.FromMe(it)
                else DataItem.ToMe(it)
            } + listOf(DataItem.Footer)
            withContext(Dispatchers.Main) { //update in main ui thread
                submitList(items)
            }
        }
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
        return when (viewType) {
            ItemViewType.MESSAGE_FROM_ME.ordinal -> FromMeViewHolder.from(parent)
            ItemViewType.MESSAGE_TO_ME.ordinal -> ToMeViewHolder.from(parent)
            else -> FooterViewHolder.from(parent)
        }
    }

    override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
        when (holder) {

            is FromMeViewHolder -> {
                val data = getItem(position) as DataItem.FromMe
                holder.bind(data.message)
            }

            is ToMeViewHolder -> {
                val data = getItem(position) as DataItem.ToMe
                holder.bind(data.message)
            }

            is FooterViewHolder -> {
            }
        }
    }

    override fun getItemViewType(position: Int): Int {
        return when (getItem(position)) {
            is DataItem.FromMe -> ItemViewType.MESSAGE_FROM_ME.ordinal
            is DataItem.ToMe -> ItemViewType.MESSAGE_TO_ME.ordinal
            is DataItem.Footer -> ItemViewType.FOOTER.ordinal
        }
    }


    class FromMeViewHolder private constructor(val binding: ItemAccountHistoryNextContentBinding) :
        RecyclerView.ViewHolder(binding.root) {

        fun bind(message: Message) {
            itemView.apply {
                tv_message_from_me.text = message.content
            }
        }

        companion object {
            fun from(parent: ViewGroup): RecyclerView.ViewHolder {
                val view = LayoutInflater.from(parent.context).inflate(R.layout.itemview_message_from_me, parent, false)
                return ToMeViewHolder(view)
            }
        }


    }

    class ToMeViewHolder (view: View) : RecyclerView.ViewHolder(view) {

        fun bind(message: Message) {
            itemView.apply {
                tv_message_to_me.text = message.content
            }
        }

        companion object {
            fun from(parent: ViewGroup): RecyclerView.ViewHolder {
                val view = LayoutInflater.from(parent.context).inflate(R.layout.itemview_message_to_me, parent, false)
                return ToMeViewHolder(view)
            }
        }

    }

    class FooterViewHolder (view: View) : RecyclerView.ViewHolder(view) {
        companion object {
            fun from(parent: ViewGroup): RecyclerView.ViewHolder {
                val view = LayoutInflater.from(parent.context).inflate(R.layout.itemview_footer, parent, false)
                return FooterViewHolder(view)
            }
        }
    }

    class DiffCallback : DiffUtil.ItemCallback<DataItem>() {
        override fun areItemsTheSame(oldItem: DataItem, newItem: DataItem): Boolean {
            return oldItem.id == newItem.id
        }

        override fun areContentsTheSame(oldItem: DataItem, newItem: DataItem): Boolean {
            return oldItem == newItem
        }

    }
}

sealed class DataItem {

    abstract val id: Long
    abstract val isFromMe: Boolean

    data class FromMe(val message: Message) : DataItem() {
        override val id = message.id
        override val isFromMe = message.isFromMe
    }

    data class ToMe(val message: Message) : DataItem() {
        override val id = message.id
        override val isFromMe = message.isFromMe
    }

    object Footer : DataItem() {
        override val id = Long.MIN_VALUE
        override val isFromMe = false
    }

}

<<:  Day-7 带着童年的好朋友任天堂红白机、重新在 HDMI 电视上发光吧!

>>:  [Day8] 从入门到入狱! 用Python窃听电脑键盘事件!

Day.21 「物件也有继承问题?」 —— JavaScript 继承 与 原型链

我们每新增一个函式,浏览器都会向函式内新增一个属性叫 prototype function Per...

30天轻松学会unity自制游戏-让Player动起来

按照之前的进度制作,现在按下▶Player应该会魔性地扭动起来,但就没有其他效果了,接下来就改造Pl...

[第二十天]从0开始的UnityAR手机游戏开发-介绍Animator02

本次将延续前一章节的教学 点选Cube Animation往CubeAttack Animation...

Powershell 入门添加参数帮助信息

我们写的脚本不仅仅是自己使用,有时需要分享给别人使用。这种情况下,帮助信息可以更好地帮助使用者,使用...

如何用笔电连线到HPE服务器

请问各位电脑高手 我现在把我的笔电跟HPE服务器的IP网段都已经设定成一样的 但是开启网页输入还是无...