Problem
昨天我們提到ListAdapter + DiffUtil在一般RecyclerView的基本使用。
而實際上工作中我們經常會需要在RecyclerView上顯示不同的itemView,
我們會添加Footer、Header,或是不同樣式的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
)
首先,因為會展示不同的項目,我們不餵ListAdapter吃data class了,
改吃自己創建的sealed class - 這邊我們命名為DataItem。
class MessageAdapter() : ListAdapter<DataItem, RecyclerView.ViewHolder>(DiffCallback()) {}
我們添加一個sealed class,
這個sealed class負責用來控管不同item的型態類別。
這邊我們列舉出Item和Footer這兩個型態(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。
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
}
}
創建一個enum class列舉需展示的所有viewType,
enum class ItemViewType {
MESSAGE_FROM_ME, MESSAGE_TO_ME, FOOTER
}
這邊列舉viewType,是給Adapter判斷要展示哪個ViewHolder來使用的。
因為我們有不同的型別需判斷,
所以我們要覆寫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,繼續往下看下去。
onCreateViewHolder與onBindViewHolder判斷 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 -> {
}
}
}
ViewHolder為FromMe、ToMe及Footer創建對應的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)
}
}
}
最後一步了!
現在因為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)
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
}
}