우리는 리스트를 나타내기 위해서 RecyclerView를 사용합니다. 그리고 리스트의 데이터에 변화가 있으면 notifyDataChanged() 메서드를 사용해서 알리게 됩니다.
하지만 notifyDataChanged 메서드는 새로운 item 인스턴스를 생성하기 때문에 비용이 많이 발생합니다. 이는 화면 깜빡임을 유발하게 될 수 있습니다. 따라서 성능 개선을 원한다면 변경되는 특정 데이터만 notify해주도록 합니다.
하지만 변경이 한 개의 데이터가 아니고 여러개의 데이터가 한번에 변경된 경우에는 어쩔 수 없이 notifyDataChanged() 를 사용해야 합니다. 이런 경우에 들게 되는 비용을 줄이기 위해서 등장하게 된 것이 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개의 추상 메서드
1개의 비 추상 메서드
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)
}
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)
}
}
AsyncListDiffer을 더 편리하게 사용하도록 한 Wrapper 클래스가 ListAdapter입니다.
ListAdapter는 DiffUtil을 활용하여 리스트를 업데이트하는 기능이 추가된 Adapter입니다. AsyncDiffConfig를 받을 수도 있고, 흔히 사용하는 DiffUtil.ItemCallback을 받아서 사용하고 있다.
ListAdapter는 사용자를 위해 목록을 추적하고 목록이 업데이트 될 때 어댑터에 알린다.
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이-뭔지-설명해주시겠어요