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)
}
}