Update inner view inside RecyclerView item with LiveData, Room and DiffUtil

608 Views Asked by At

I have Message table in Room with following structure: id, text, animRes, readStatus.

Then I subscribe to livedata exposed by room to display this item in recyclerview with diffutil adapter.

When new message appeared in RV, animation start playing. If I add readStatus to diffutil areContentsTheSame function, my animation would restart each time readStatus changes, that why I can't just use diffutil to redraw full view.

Is there any way to update only one inner indicator view inside RV items, when value in the room changes? How this works in messenger, where changing of read status doesn't restart animation (stickers or gif)?

Thanks)

1

There are 1 best solutions below

0
On BEST ANSWER

Finally, I found an answer)

Implement DiffUtil (getChangePayload should return something if you want partial updating, otherwise return null for full rebind):

    class DiffUtilCallback : DiffUtil.ItemCallback<MessageModel>() {
        override fun getChangePayload(oldItem: MessageModel, newItem: MessageModel): Any? {
            if (oldItem.readStatus!=newItem.readStatus) {
                return Bundle().apply {
                    putInt("readStatus", newItem.readStatus)
                }
            }
            return null
        }
        override fun areItemsTheSame(oldItem: MessageModel, newItem: MessageModel): Boolean {
            return oldItem == newItem
        }

        override fun areContentsTheSame(oldItem: MessageModel, other: MessageModel): Boolean {
            oldItem.apply {
                return id == other.id && text == other.text && animRes == other.animRes && readStatus == other.readStatus
            }
        }
    }

Then in adapter (example with ViewBinding from here: https://stackoverflow.com/a/60427658):

    class MessageAdapter : ListAdapter<MessageModel, MessageViewHolder>(DiffUtilCallback()) {

        override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MessageViewHolder {
                val binding = InboundMessageBinding.inflate(LayoutInflater.from(parent.context), parent, false)
                return MessageViewHolder(binding)
        }

        override fun onBindViewHolder(holder: MessageViewHolder, position: Int) {
            holder.bind(getItem(position))
        }

        override fun onBindViewHolder(holder: MessageViewHolder, position: Int, payloads: MutableList<Any>) {
            if (payloads.isNotEmpty()) {
                val item = payloads[0] as Bundle
                val readStatus = item.getInt("readStatus")
                holder.binding.status.text = readStatus.toString()
                return
            }
            super.onBindViewHolder(holder, position, payloads)
        }
    }

We call onBindViewHolder constructor only if getChangePayload from diffUtil return null. Otherwise just change target view, simple and effective)

Upd: If payload changes while add animation playing, two animations overlap each other and the result is not nice.

This can be fixed in this way:

    messageRv.itemAnimator = object : DefaultItemAnimator()  {
        override fun animateChange(oldHolder: RecyclerView.ViewHolder?, newHolder: RecyclerView.ViewHolder?, fromX: Int, fromY: Int, toX: Int, toY: Int): Boolean {
            if ((oldHolder as? MessageViewHolder)?.itemId==(newHolder as? MessageViewHolder)?.itemId) { //don't animate same view to prevent blinking when only readStatus changes
                dispatchChangeFinished(newHolder, true) //for correct next animation
                return true
            }
            return super.animateChange(oldHolder, newHolder, fromX, fromY, toX, toY)
        }
    }