버튼 수집상

[안드로이드] ListAdapter DiffUtil 제대로 쓰기 - 2 본문

TIL - 안드로이드

[안드로이드] ListAdapter DiffUtil 제대로 쓰기 - 2

cocokaribou 2023. 7. 13. 14:14

1편에서 ListAdapter에 submitList를 하면서 기존 리스트를 업데이트할 때

리스트 깊은 복사deep copy를 해야 하는 이유에 대해 적었다.

 

이번엔 ListAdapter DiffUtil의 각 함수를 자세히 알아보겠다.

샘플 코드 전체는 1편에 있다.

 

DiffUtil.ItemCallback 에 구현해야하는 함수 2가지가 있다.

areContentsTheSame : 리스트 요소의 객체 주소를 비교한다
areItemsTheSame : 리스트 요소의 필드값을 비교한다

 

함수 이름만 보면 하는 일이 반대가 돼야할 것 같은데 아무튼 그렇다.

areItemsTheSame에서 변경사항을 감지하고 싶은 값을 비교해서 UI를 업데이트할 수 있다.

예제 데이터 SimpleObject의 isChecked 값을 비교해보겠다.

class SimpleAdapter : ListAdapter<SimpleObject, SimpleAdapter.SimpleHolder>(object : DiffUtil.ItemCallback<SimpleObject>() {
    override fun areContentsTheSame(oldItem: SimpleObject, newItem: SimpleObject): Boolean {
        return oldItem == newItem
    }

    // isChecked가 변경된 뷰홀더 체크
    override fun areItemsTheSame(oldItem: SimpleObject, newItem: SimpleObject): Boolean {
        return oldItem.isChecked == newItem.isChecked
    }

}) {
    override fun getItemViewType(position: Int) = position

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SimpleHolder =
        SimpleHolder(LayoutInflater.from(parent.context).inflate(R.layout.item_grid, parent, false))

    // submitList 로 업데이트되면 onBindViewHolder 호출
    override fun onBindViewHolder(holder: SimpleHolder, position: Int) {
        val isChecked = currentList[position].isChecked
        val bgColor = if (isChecked) Color.parseColor("#30ff0000") else Color.parseColor("#00ff0000")
        holder.itemView.findViewById<ImageView>(R.id.iv).setColorFilter(bgColor)

        holder.itemView.findViewById<TextView>(R.id.title).text = currentList[position].name
        holder.itemView.setOnClickListener {
            // 클릭 이벤트 (api 호출)
            EventBus.fire(ClickEvent(position, "isClicked"))
        }

    }
}

그러면 submitList로 리스트가 업데이트 됐을 때

areItemsTheSame 으로 isChecked 변경여부를 읽고 onBindViewHolder가 호출된다.

그런데 보다시피 문제가 있다.

isChecked가 객체를 대변하는 유니크한 값이 아니다보니 변경할 객체를 제대로 찾아가지 못한다.

그리고 onBindViewHolder로 뷰홀더가 다시 그려질 때 반짝이는 것을 볼 수 있다.

 

UI를 부분 업데이트할 땐 DiffUtil.ItemCallback의 getChangePayload 함수로 처리하는 것이 훨씬 자연스럽다.

getChangePayload : areItemsTheSame에서 비교하는 필드값은 같은데, 요소 객체가 바뀌었을 때 호출된다. 변경값을 리턴한다.

 

getChangePayload에서 리턴한 변경값(payload)은

onBindViewHolder(holder, position, payloads) 함수를 구현해서 전달받는다.

class SimpleAdapter : ListAdapter<SimpleObject, SimpleAdapter.SimpleHolder>(object : DiffUtil.ItemCallback<SimpleObject>() {
    override fun areContentsTheSame(oldItem: SimpleObject, newItem: SimpleObject): Boolean {
        return oldItem == newItem
    }

    // 객체를 대변하는 유니크한 값 비교
    override fun areItemsTheSame(oldItem: SimpleObject, newItem: SimpleObject): Boolean {
        return oldItem.name == newItem.name
    }

    // isChecked가 true인 뷰홀더 체크
    override fun getChangePayload(oldItem: SimpleObject, newItem: SimpleObject): Any? {
        return if (oldItem.isChecked == newItem.isChecked) {
            super.getChangePayload(oldItem, newItem)
        } else {
            newItem.isChecked
        }
    }
}) {
    override fun getItemViewType(position: Int) = position

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SimpleHolder =
        SimpleHolder(LayoutInflater.from(parent.context).inflate(R.layout.item_grid, parent, false))

    // getChangePayload 에서 호출되지 않는다
    override fun onBindViewHolder(holder: SimpleHolder, position: Int) {
        holder.itemView.findViewById<TextView>(R.id.title).text = currentList[position].name
        holder.itemView.setOnClickListener {
            // 클릭 이벤트 (api 호출)
            EventBus.fire(ClickEvent(position, "isClicked"))
        }
    }

    // getChangePayload에서 payload 값을 받아온다
    override fun onBindViewHolder(holder: SimpleHolder, position: Int, payloads: MutableList<Any>) {
        if (payloads.isEmpty()) {
            super.onBindViewHolder(holder, position, payloads)
        } else {
            (payloads[0] as? Boolean)?.let {
                holder.turnOn(it)
            } ?: run {
                super.onBindViewHolder(holder, position, payloads)
            }
        }
    }

    class SimpleHolder(private val view: View) : RecyclerView.ViewHolder(view) {
        fun turnOn(isChecked: Boolean) {
            val bgColor = if (isChecked) Color.parseColor("#30ff0000") else Color.parseColor("#00ff0000")
            view.findViewById<ImageView>(R.id.iv).setColorFilter(bgColor)
        }
    }
}

getChangePayload의 리턴 타입은 Any?로 정의돼있어서

변경사항을 원하는 형태로 전달할 수 있다.

payload를 이용하면 UI가 반짝거리지 않고 깔끔하게 업데이트 되는 것을 볼 수 있다.

 

그동안 DiffUtil을 아무 생각없이 구현하고는 했는데,

UI를 부분 업데이트할 일이 더 많은 이상, 앞으로는 getChangePayload를 애용하게 될 것 같다.

 

 

다음 글

 

[안드로이드] ListAdapter DiffUtil 제대로 쓰기 - 3

지난 글에서 ListAdapter DiffUtil 의 getChangePayload 활용법에 대해 적었다. [안드로이드] ListAdapter DiffUtil 제대로 쓰기 - 2 1편에서 ListAdapter에 submitList를 하면서 기존 리스트를 업데이트할 때 리스트 깊은

collectingbuttons.tistory.com

 

728x90