[Android] RecyclerView DiffUtil 1. - Basic

Twaun·2022년 6월 27일
0

Android

목록 보기
14/24

DiffUtil이 뭔가요?

recyclerView 를 사용할 때 리스트에 변경이 있을 시 notifyDataSetChanged() 를 사용하여 리스트를 업데이트 하였다. 하지만 이는 이전 리스트를 새 리스트로 바뀌는 작업으로 데이터 하나만 바뀌더라도 모든 리스트가 새로 생성되고 그려지는 매우 비효율적인 메서드 이다.

이 비효율적인 작업을 해결하기 위해 RecyclerView에 DiffUtil 클래스 가 존재한다. 이 클래스는 Eugene W. Myers's difference algorithm을 이용하여 두 리스트 간의 최소한의 업데이트 수를 찾는다. 그리고 찾은 데이터들에 대해서 RecyclerView 는 업데이트를 진행하면 된다.

알고리즘 성능

또한 알고리즘 성능은 O(N + D^2)이 된다.
(D = the length of the edit script)

리스트에 움직임이 가능하다면, O(MN)가 추가된다.
(M = the total number of added items)
(N = the total number of removed items)

이에 움직임을 제한하면 성능을 향상시킬 수 있을 것이다.

사용 방법

준비물

  1. Data Class
  2. DiffUtil Class
  3. ViewHolder
  4. Adapter

1. Data Class

data class BodyPart(
    val id: Int, // 각 데이터의 고유한 id
    val title: String
)

2. DiffUtil Class

이전 리스트와 새 리스트의 차이를 계산하기 위해 비교할 두 리스트를 받고, DiffUtil.Callback()을 구현한다.

DiffUtil.Callback 은 4개의 abstract 함수를 구현해야한다.

  • getOldListSize()
    이전 리스트의 크기를 반환
  • getNewListSize()
    새 리스트의 크기를 반환
  • areItemsTheSame(oldItemPosition: Int, newItemPosition: Int)
    두 아이템이 같은 객체인지 True/False 반환
  • areContentsTheSame(oldItemPosition: Int, newItemPosition: Int)
    두 아이템이 같은 데이터를 갖고 있는지 True/False 반환한다.
class BodyPartDiffUtilCallback(
    private val oldList: List<BodyPart>,
    private val newList: List<BodyPart>,
) : DiffUtil.Callback(){
    override fun getOldListSize(): Int {
        return oldList.size
    }

    override fun getNewListSize(): Int {
        return newList.size
    }

    override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
        return oldList[oldItemPosition].id == newList[newItemPosition].id
    }

    override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
        return oldList[oldItemPosition].id == newList[newItemPosition].id
    }
}

3. ViewHolder

기존의 ViewHolder 구현과 동일하다.

class BodyPartViewHolder(
    private val binding: ItemBodyPartBinding
) : RecyclerView.ViewHolder(binding.root) {
    fun bind(
        data: BodyPart,
        onClickListener: (()->Unit)? = null
    ) {
        binding.textView.text = data.title
        binding.buttonMove.setOnSingleClickListener {
            onClickListener?.invoke()
        }
    }
}

4. Adapter

RecyclerView Adapter에 새 리스트를 받는 update() 함수를 만든 후 위에서 생성한 DiffUtil 콜백함수를 생성하여 DiffUtil.calculateDiff에 전달하면 연산을 시작한다.

연산이 완료되면 리스트를 업데이트하고
diffResult.dispatchUpdatesTo(adapter: this)를 호출하면 리사이클러뷰를 알아서 갱신해준다!!

class BodyPartAdapter(
    var moveOnClickedListener: (()->Unit)? = null
) : RecyclerView.Adapter<BodyPartViewHolder>() {

    private val itemList = mutableListOf<BodyPart>()

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BodyPartViewHolder {
        return BodyPartViewHolder(ItemBodyPartBinding.inflate(LayoutInflater.from(parent.context), parent, false))
    }

    override fun onBindViewHolder(holder: BodyPartViewHolder, position: Int) {
        holder.bind(itemList[position]) {
            moveOnClickedListener?.invoke()
        }
    }

    override fun getItemCount(): Int {
        return itemList.size
    }


    fun update(newItemList: List<BodyPart>) {
        val diffCallback  = BodyPartDiffUtilCallback(itemList, newItemList)
        val diffResult = DiffUtil.calculateDiff(diffCallback)

        itemList.clear()
        itemList.addAll(newItemList)

        diffResult.dispatchUpdatesTo(this)
    }

}

이제 외부에서는 리스트에 변동이 있을 때마다 update() 에 리스트를 담아서 호출해주기만 하면 된다!! 이제 notifyDataSetChanged()를 안써도 되고 속도도 매우 빨라졌을 것이다!!

하지만.. 🤔

이 알고리즘이 아무리 좋다고 해도 비교할 리스트 데이터가 너무 많아지게 되면 연산이 길어질 수 있어 UI 쓰레드에 실행하기엔 위험이 따른다.

때문에 안드로이드 개발 문서 를 보면 연산 실행을 백그라운드 쓰레드에서 실행하기를 권장되고 있다.

그렇다면 백그라운드 쓰레드에서 실행하는 방법을 [DiffUtil with Async] 에서 알아보자!!

profile
Android Developer

0개의 댓글