[Android] RecyclerView(3) - DiffUtil

혜령·2022년 1월 11일
0

Android 공부하기

목록 보기
5/8

왜 사용할까?

우리는 리스트를 나타내기 위해서 RecyclerView를 사용합니다. 그리고 리스트의 데이터에 변화가 있으면 notifyDataChanged() 메서드를 사용해서 알리게 됩니다.

하지만 notifyDataChanged 메서드는 새로운 item 인스턴스를 생성하기 때문에 비용이 많이 발생합니다. 이는 화면 깜빡임을 유발하게 될 수 있습니다. 따라서 성능 개선을 원한다면 변경되는 특정 데이터만 notify해주도록 합니다.

하지만 변경이 한 개의 데이터가 아니고 여러개의 데이터가 한번에 변경된 경우에는 어쩔 수 없이 notifyDataChanged() 를 사용해야 합니다. 이런 경우에 들게 되는 비용을 줄이기 위해서 등장하게 된 것이 DiffUtil입니다.

DiffUtil이란?

DiffUtil은 리스트에 나타낼 아이템들을 old item과 new item으로 나누어 두 목록의 차이를 계산하여 업데이트되는 목록을 출력하는 유틸리티 클래스입니다. 변한 아이템을 탐지하고 알아서 notify를 해주게 되므로 개발하면서 아이템이 변하는 것을 크게 신경쓰지 않아도 됩니다.

DiffUtil은 최소 업데이트 수를 계산하기 위해서 Eugene W.Myners의 차이 알고리즘을 사용한다고 합니다.

(N: 리스트 크기 , D: 추가 및 제거 된 항목 수)

공간복잡도 : O(N)

시간복잡도 : O(N + D^2)

M의 추가항목과 N의 제거항목을 찾게 될 : O(MN)의 추가 시간복잡도

위에서 알 수 있듯이 목록이 많으면 작업이 상당히 오래 걸릴 수 있기에 백그라운드 스레드에서 실행을 설정하고 DiffUtil.DiffResult를 가져와서 메인스레드에서 RecyclerView에 적용하는게 좋습니다.

구현 제약으로 목록의 최대 크기는 2²⁶개로 제한 되어 있다고 합니다.

사용법

DiffUtil.Callback은 추상 클래스입니다. 두 목록의 차이를 계산하는 동안 DiffUtil에 의해 콜백 클래스로 사용됩니다. 이는 4가지의 추상 메서드와 1가지의 비 추상 메서드를 가지고 있습니다.

DiffUtil 클래스의 사용을 위해서는 이전의 리스트와 바뀐 후의 리스트를 가지고 있어야 합니다. 그리고 4가지 메서드를 override 해줍니다.

4개의 추상 메서드

  • getOldListSize() : 바뀌기 전 리스트의 크기를 반환합니다.
  • getNewListSize() : 바뀐 후 리스트의 크기를 반환합니다.
  • areItemsTheSame(oldPosition:Int, newPosition:Int) : 두 객체가 동일한 항목을 나타내는지 확인합니다. 만일 true라면 다음 비교를 하고, false라면 리스트 갱신 시 화면이 깜빡거리는 현상이 발생할 수 있습니다. 즉, areItemsTheSame을 잘못 정의한다면 다시 새로 만들게 되어서 notifyDataSerChanged()와 다를 바 없이 집니다.
  • areContentsTheSame(oldPosition:Int, newPosition:Int) : 두 항목의 데이터가 같은지 확인한다. 해당 메서드는 areItemsTheSame()에서 true 인 경우에만 호출합니다. 같은 id값을 가졌더라도 내부의 값이 달려졌다면 변경된 것이므로 그것을 확인해야 한다. 최종적으로 false인 item에 대해서만 onBindViewHolder 메서드가 호출됩니다.

1개의 비 추상 메서드

  • getChangePayload(oldPosition:Int, newPosition:Int) : areItemsTheSame == true && areContentsTheSame==false일 경우에 호출되어 변경 내용에 대한 페이로드를 가져옵니다.
class DiffUtilCallback(
		private val oldData: List<Any>,
		private val newData: List<Any>
) : DiffUtil.Callback() {

		override fun getOldListSize(): Int = oldData.size

		override fun getNewListSize(): Int = newData.size

		override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
				val oldItem = oldData[oldItemPosition]
				val newItem = newData[newItemPosition]
		
				return oldItem.id == newItem.id
		}
		
		override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
				val oldItem = oldData[oldItemPosition]
				val newItem = newData[newItemPosition]
				
				return oldItem == newItem
		}
}

이렇게 클래스를 생성한 후에는 해당 부분을 adapter내에서 데이터를 설정하는 부분에 넣어주면 됩니다.

만든 클래스를 통해서 콜백을 받습니다. 이 콜백을 다시 DiffUtil.calculateDiff()에 전달하여 결과를 받게 됩니다. 이 결과에는 기존 목록이 새로운 목록으로 변환하기 위한 정보를 포함되어 있습니다. 이 결과를 dispatchUpdatesTo()메서드를 통해서 업데이트를 요청하게 됩니다.

private fun calDiff(newTiles: MutableList<Tile>) {
        val subjectDiffUtilCallback = SubjectDiffUtilCallback(dataSet, newTiles)
        val diffResult: DiffUtil.DiffResult = DiffUtil.calculateDiff(tileDiffUtilCallback)
        diffResult.dispatchUpdatesTo(this)
    }

AsyncListDiffer

DiffUtil을 사용하는 경우에 아이템 수가 많은 경우 연산 시간이 길어질 수 있습니다. 따라서 백그라운드에서 처리를 하는 것이 권장됩니다. 이를 돕기 위해서 등장한 클래스가 AsyncListDiffer입니다.

따로 백그라운드 작업을 하도록 추가하지 않아도, AsyncListDiffer을 통해서 구현해보면, AsyncListDiffer가 내부적으로 diff 계산을 백그라운드 스레드로 처리한 다음 업데이트까지 해줍니다. 따라서 adapter을 더 깔끔하게 사용할 수 있게 됩니다.

class UserAsyncDifferAdapter : RecyclerView.Adapter<UserViewHolder>() {
    private val asyncDiffer = AsyncListDiffer(this, UserDiffItemCallback())

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = UserViewHolder(
        ItemUserBinding.inflate(LayoutInflater.from(parent.context), parent, false)
    )

    override fun onBindViewHolder(holder: UserViewHolder, position: Int) =
        holder.bind(asyncDiffer.currentList[position])

    override fun getItemCount() = asyncDiffer.currentList.size

    fun replaceItems(newUser: List<User>) {
        asyncDiffer.submitList(newUser)
    }
}

ListAdapter

AsyncListDiffer을 더 편리하게 사용하도록 한 Wrapper 클래스가 ListAdapter입니다.

ListAdapter는 DiffUtil을 활용하여 리스트를 업데이트하는 기능이 추가된 Adapter입니다. AsyncDiffConfig를 받을 수도 있고, 흔히 사용하는 DiffUtil.ItemCallback을 받아서 사용하고 있다.

ListAdapter는 사용자를 위해 목록을 추적하고 목록이 업데이트 될 때 어댑터에 알린다.

  • getItem(position: Int) : ListAdapter 내부 List Indexing을 할 때 활용된다.
  • getCurrentList() : ListAdapter가 가지고 있는 리스트를 가져올 때 사용한다.
  • submitList(MutableList list) : 리스트 항목을 변경하고 싶을 때 사용한다.
class UserListAdapter : ListAdapter<User, UserViewHolder>(diffUtil) {

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = UserViewHolder(
        ItemUserBinding.inflate(LayoutInflater.from(parent.context), parent,false)
    )

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

    fun replaceItems(items: List<User>) {
        submitList(items)
    }

    companion object {
        val diffUtil = object: DiffUtil.ItemCallback<User>() {
            override fun areContentsTheSame(oldItem: User, newItem: User) =
                oldItem == newItem

            override fun areItemsTheSame(oldItem: User, newItem: User) =
                oldItem.name == newItem.name
        }
    }
}

Reference
https://velog.io/@jaeyunn_15/Android-DiffUtil이-뭔지-설명해주시겠어요

profile
안녕하세요

0개의 댓글