💡 Intro
DiffUtil은 RecyclerView의 리스트를 업데이트 할 수 있는 방법 중 하나다.
우테코를 하면서 이 녀석의 존재를 알고 있긴 했는데 정확히 어떤 녀석이고 어떤 방법으로 리스트를 변경하는지는 몰랐다. 마침 지금 진행하고 있는 프로젝트에서 DiffUtil을 사용하고 있으니 자세하게 알아보려고 한다.
RecyclerView 데이터 업데이트 방법
DiffUtil을 사용하지 않아도 RecyclerView의 리스트를 업데이트 하는 방법은 많다.
대표적인 메서드로으로 notifyDataSetChanged가 생각날 것이고 이외에도 notify 로 시작하는 메서드들이 많다.
notifyDataSetChanged에 대해서 공식문서를 살펴보면 해당 메서드는 최후의 수단으로 사용하라고 한다.
데이터 변경 이벤트는 항목 변경과 구조 변경 두 가지가 있다.
- 리스트 아이템의 정보가 바뀌어 항목이 변경된 경우
- 리스트에 아이템이 추가, 삭제, 위치 변경이 되어 구조적인 변경이 일어난 경우
notifyDataSetChanged는 2번에 해당하여 리사이클러뷰의 있는 모든 뷰의 배치와 구성을 변경하는 작업을 수행한다.
아이템의 정보만 바뀐 경우 notifyDataSetChanged를 사용하면 불필요하게 뷰의 배치와 구성을 변경하게 되는 것이다. 그래서 최후의 수단으로 사용하라고 언급하고 있으며, 아래 보이는 notify 메서드를 알맞게 사용하면 좋다고 이야기한다.
notifyItemChanged(int)
notifyItemInserted(int)
notifyItemRemoved(int)
notifyItemRangeChanged(int, int)
notifyItemRangeInserted(int, int)
notifyItemRangeRemoved(int, int)
❓ DiffUtil
리스트의 데이터는 하나만 업데이트 될 수도 있고, 대량의 데이터가 업데이트 될 수도 있고, 모든 데이터가 업데이트 될 수도 있다. 즉, 리스트가 업데이트 되는 경우는 굉장히 많을 것이다. 그래서 어느 시점에 어떤 메서드를 써야 가장 효율적일지 생각하며 작성하기란 여간 골치아픈 일이 아닐 수 없다.
그래서 이를 조금 더 쉽게 만들어줄 새로운 친구 DiffUtil이 등장한다.
DiffUtil은 두 리스트의 차이를 계산하고 첫 번째 리스트에서 두 번째 리스트로 데이터를 변환하는 유틸리티 클래스다.
이름도 특이한 Eugene W. Myers's difference algorithm을 사용하여 두 리스트 사이의 변경된 부분만 업데이트한다.
DiffUtil은 리스트가 클수록 차이를 계산하는 작업이 오래걸릴 수 있기 때문에 백그라운드 스레드에서 작업을 실행하고 DiffResult를 가져와 메인 스레드의 RecyclerView에 적용하는 것이 좋다다.
사용하려면?
다음은 DiffUtil이 가지고 있는 추상 클래스 DiffUtil.Callback이다. 4개의 추상메서드를 가지고 있고 1개의 메서드를 가지고 있다.
/**
* A Callback class used by DiffUtil while calculating the diff between two lists.
*/
public abstract static class Callback {
public abstract int getOldListSize();
public abstract int getNewListSize();
public abstract boolean areItemsTheSame(int oldItemPosition, int newItemPosition);
public abstract boolean areContentsTheSame(int oldItemPosition, int newItemPosition);
@Nullable
public Object getChangePayload(int oldItemPosition, int newItemPosition) { return null; }
}
- areItemsTheSame 은 아이템이 같은 아이템인지를 확인해야 하기 때문에 고유 값이나 id 또는 해시를 사용한다.
- areContentsTheSame 은 데이터가 같은지 확인한다. areItemsTheSame 이 true를 반환하는 경우에만 호출된다.
DiffUtil.Callback을 구현한 구현체를 RecyclerView의 어댑터에서 데이터를 변경할 때 사용하면 된다.
data class Person(val age: Int, val name: String)
class PersonDiffUtilCallback(
private val oldPersonList: List<Person>,
private val newPersonList: List<Person>,
) : DiffUtil.Callback() {
override fun getOldListSize(): Int {
return oldPersonList.size
}
override fun getNewListSize(): Int {
return newPersonList.size
}
override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
return oldPersonList[oldItemPosition] === newPersonList[newItemPosition]
}
override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
return oldPersonList[oldItemPosition] == newPersonList[newItemPosition]
}
}
class PersonAdapter(
private var data: List<Person>,
) : RecyclerView.Adapter<PersonViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PersonViewHolder {
return PersonViewHolder(parent)
}
override fun onBindViewHolder(holder: PersonViewHolder, position: Int) {
holder.bind(data[position])
}
override fun getItemCount(): Int {
return data.size
}
// 변경사항을 업데이트하는 메서드
fun updateList(newData: List<Person>) {
val personDiffUtilCallback = PersonDiffUtilCallback(data, newData)
val diffResult = DiffUtil.calculateDiff(personDiffUtilCallback)
data = newData
diffResult.dispatchUpdatesTo(this)
}
}
DiffUtil.calculateDiff 메서드는 DiffResult를 반환한다.
DiffResult는 dispatchUpdatesTo 메서드를 통해 결과를 전달해줄 어댑터를 결정하고 바로 전달할 수 있다.
❓ AsyncListDiffer
여기까지 아무런 문제 없이 잘 동작하는 것처럼 보인다.
그런데 앞에서 DiffUtil은 리스트가 클수록 차이를 계산하는 작업이 오래걸릴 수 있기 때문에 백그라운드 스레드에서 작업을 실행하고 DiffResult를 가져와 메인 스레드의 RecyclerView에 적용하는 것이 좋다고 했다.
이러한 DiffUtil을 백그라운드 스레드에서 수행할 수 있게 해주는 AsyncListDiffer가 있다.
Adapter와 DiffUitil.ItemCallback을 인자로 받아 DiffUtil을 백그라운드 스레드에서 수행하고 DiffResult를 가져와 메인 스레드에서 RecyclerView에 적용한다.
class PersonDiffUtilCallback : DiffUtil.ItemCallback<Person>() {
override fun areItemsTheSame(oldItem: Person, newItem: Person): Boolean {
return oldItem.age == newItem.age && oldItem.name == newItem.name
}
override fun areContentsTheSame(oldItem: Person, newItem: Person): Boolean {
return oldItem == newItem
}
}
앞에서는 DiffUtil.Callback 추상 클래스를 구현했지만, AsyncListDiffer를 사용할 때는 DiffUtil.ItemCallback 추상 클래스를 구현해줘야 한다. 추상 메서드가 areItemsTheSame, areContentsTheSame 2개, 일반 메서드 1개를 가지고 있는 추상 클래스다.
class PersonAdapter : RecyclerView.Adapter<PersonViewHolder>() {
private val asyncListDiffer = AsyncListDiffer(this, PersonDiffUtilCallback())
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PersonViewHolder {
return PersonViewHolder(parent)
}
override fun onBindViewHolder(holder: PersonViewHolder, position: Int) {
holder.bind(asyncListDiffer.currentList[position])
}
override fun getItemCount(): Int {
return asyncListDiffer.currentList.size
}
fun updateList(newData: List<Person>) {
asyncListDiffer.submitList(newData)
}
}
Adapter가 AsyncListDiffer를 가지고 있도록 하고 submitList 메서드를 사용하여 손쉽게 새로운 리스트로 변경할 수 있다.
asyncListDiffer의 currentList를 사용하여 현재 Adapter가 가지고 있는 리스트에 접근할 수 있다.
❓ ListAdapter
Adapter에서 AsyncListDiffer를 만들어 사용하는 것보다 더 쉽게 DiffUtil을 사용할 수 있도록 도와주는 것이 ListAdapter다.
ListAdapter는 AsyncListDiffer를 래핑한 클래스라고 보면 된다.
RecyclerView.Adapter를 상속받고 있기 때문에 RecyclerView.Adapter 대신 ListAdapter를 사용할 수 있고, 내부적으로 가지고 있는 mDiffer를 사용하여 AsyncListDiffer 객체를 생성하지 않고 백그라운드 스레드에서 DiffUtil의 비교 연산을 수행할 수 있다.
T에는 데이터의 타입을, VH에는 RecyclerView.ViewHolder를 상속받은 Adapter의 ViewHolder를 넣어주면 된다.
class PersonListAdapter : ListAdapter<Person, PersonViewHolder>(PersonDiffUtilCallback()) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PersonViewHolder {
return PersonViewHolder(parent)
}
override fun onBindViewHolder(holder: PersonViewHolder, position: Int) {
holder.bind(currentList[position])
}
fun updateList(newData: List<Person>) {
submitList(newData)
}
}
PersonDiffUtilCallback은 앞에서 정의한 그대로 사용하면 된다.
ListAdapter 자체에서 submitList 메서드를 지원하기 때문에 외부로 submitList 메서드를 노출시키지 않고 바로 Adapter에서 submitList 메서드를 이용하여 데이터를 변경할 수 있다.
TL;DR
DiffUtil
notify* 메서드를 상황에 맞게 사용해야 하는 어려움 때문에 등장한 클래스다. DiffUtil.Callback 에 정의해준 방법대로 두 리스트의 차이를 계산하고 결과를 바로 RecyclerView.Adapter에 알릴 수 있다.
AsyncListDiffer
DiffUtil의 작업을 백그라운드 스레드에서 수행할 수 있도록 도와주는 클래스다. DiffResult를 가져와 메인 스레드에서 RecyclerView에 적용한다.
ListAdapter
AsyncListDiffer을 내부적으로 가지고 있어 AsyncListDiffer의 객체를 생성하지 않고 DiffUtil의 장점을 편하게 누릴 수 있도록 도와준다.
참고링크