
체크박스를 클릭할 때 마다, 연관이 없는 뷰가 깜빡이는 현상이 발생하였다.
처음에는 dataBinding을 사용하고 있기 때문에, 뷰 업데이트가 느려서 생기는 문제라 생각했다.
하지만 아이템 전체가 아닌, 아이템 내부의 특정 뷰만 업데이트를 해야했다
ListAdapter는 데이터 단위의 변경을 감지하고 뷰 홀더 단위로 업데이트를 수행한다.
즉, 바뀐 데이터에 대해 뷰 아이템 전체가 변경된다.
밑의 상품 목록에서 첫 번째 상품과는 다르게, 2번째 상품이 깜빡이는 이유가 위와 같다.
버튼 뷰과 가격만 업데이트 되어야 하는데, 이미지도 클릭 이벤트에 영향을 받아 불필요한 업데이트가 진행되고 있다.

그렇다면, 어떻게 특정 뷰만 업데이트 할 수 있을까?
DiffUtil.ItemCallback의 주요 메서드는 다음과 같다.
이 중에서, 우리가 봐야할 것은 getChangePayload이다.
onBindViewHolder의 파라미터인 payload는 notifyItemChanged 혹은 notifyItemRangeChanged 에서 전달된 payload 객체들의 병합된 리스트다.
즉, 변경 사항을 나타내는 데이터들의 병합된 리스트이다.
payload는 특정 데이터 변경 정보를 전달하기 위한 것이며, ViewHolder가 이전 데이터에 바인딩된 상태에서 변경된 부분만 업데이트할 수 있도록 한다.
만약 체크 여부가 바뀌었을 경우 IsCheckChanged를, 그 이외의 데이터가 바뀌었을 경우 CouponUpdate를 반환한다.
onBindViewHolder의 파라미터로 들어온 payloads의 타입을 검사하여, 각 상황에 따라 뷰를 어떻게 업데이트할지 정의한다.
이렇게 하면, 불필요하게 뷰를 업데이트하는 상황을 방지할 수 있다.
companion object {
private val diffCallback = object : DiffUtil.ItemCallback<CouponUiModel>() {
override fun areItemsTheSame(oldItem: CouponUiModel, newItem: CouponUiModel): Boolean {
return oldItem.id == newItem.id
}
override fun areContentsTheSame(oldItem: CouponUiModel, newItem: CouponUiModel): Boolean {
return oldItem == newItem
}
override fun getChangePayload(oldItem: CouponUiModel, newItem: CouponUiModel): Any? {
return when {
// 체크 상태가 다르다면, checkbox만 업데이트
oldItem.isChecked != newItem.isChecked -> CouponPayload.IsCheckChanged(newItem.isChecked)
// 객체가 동등하지 않다면, item view 전체를 업데이트
oldItem != newItem -> CouponPayload.CouponUpdate(newItem)
else -> super.getChangePayload(oldItem, newItem)
}
}
}
}
sealed interface CouponPayload {
data class IsCheckChanged(val isChecked: Boolean) : CouponPayload
data class CouponUpdate(val couponUiModel: CouponUiModel) : CouponPayload
}
override fun onBindViewHolder(
holder: CouponViewHolder,
position: Int,
) {
holder.onBind(getItem(position))
}
// 파라미터가 다른, 같은 이름의 메서드를 만들 수 있다.
override fun onBindViewHolder(
holder: CouponViewHolder,
position: Int,
payloads: MutableList<Any>
) {
if (payloads.isNotEmpty()) {
payloads.forEach { payload ->
when (payload) {
is CouponPayload.IsCheckChanged -> holder.updateCheck(payload.isChecked)
is CouponPayload.CouponUpdate -> holder.onBind(payload.couponUiModel)
}
}
} else {
onBindViewHolder(holder, position)
}
}
class CouponAdapter(
private val onClick: CouponClickListener,
) : ListAdapter<CouponUiModel, CouponAdapter.CouponViewHolder>(diffCallback) {
class CouponViewHolder(
private val binding: ItemCouponBinding,
private val onClick: CouponClickListener,
) : RecyclerView.ViewHolder(binding.root) {
fun onBind(item: CouponUiModel) {
with(binding) {
tvCouponItemTitle.text = item.title
tvCouponItemExpireDate.text =
itemView.context.getString(R.string.coupon_expire).format(item.expireDate)
.format(item.availableStartTime, item.availableEndTime)
tvCouponItemMinimumOrderPrice.text =
itemView.context.getString(R.string.coupon_minimum_amount)
.format(item.minimumAmount)
isChecked = item.isChecked
couponClickListener = onClick
couponId = item.id
}
}
fun updateCheck(isChecked: Boolean) {
binding.isChecked = isChecked
}
}
override fun onCreateViewHolder(
parent: ViewGroup,
viewType: Int,
): CouponViewHolder {
val binding = ItemCouponBinding.inflate(LayoutInflater.from(parent.context), parent, false)
return CouponViewHolder(binding, onClick)
}
override fun onBindViewHolder(
holder: CouponViewHolder,
position: Int,
) {
holder.onBind(getItem(position))
}
override fun onBindViewHolder(
holder: CouponViewHolder,
position: Int,
payloads: MutableList<Any>
) {
if (payloads.isNotEmpty()) {
payloads.forEach { payload ->
when (payload) {
is CouponPayload.IsCheckChanged -> holder.updateCheck(payload.isChecked)
is CouponPayload.CouponUpdate -> holder.onBind(payload.couponUiModel)
}
}
} else {
onBindViewHolder(holder, position)
}
}
companion object {
private val diffCallback = object : DiffUtil.ItemCallback<CouponUiModel>() {
override fun areItemsTheSame(oldItem: CouponUiModel, newItem: CouponUiModel): Boolean {
return oldItem.id == newItem.id
}
override fun areContentsTheSame(oldItem: CouponUiModel, newItem: CouponUiModel): Boolean {
return oldItem == newItem
}
override fun getChangePayload(oldItem: CouponUiModel, newItem: CouponUiModel): Any? {
return when {
oldItem.isChecked != newItem.isChecked -> CouponPayload.IsCheckChanged(newItem.isChecked)
oldItem != newItem -> CouponPayload.CouponUpdate(newItem)
else -> super.getChangePayload(oldItem, newItem)
}
}
}
}
}
sealed interface CouponPayload {
data class IsCheckChanged(val isChecked: Boolean) : CouponPayload
data class CouponUpdate(val couponUiModel: CouponUiModel) : CouponPayload
}