RecyclerView를 사용할 때, 리스트가 update되어 RecyclerView에 반영해야할 때, 흔히 쓰는 method는 notifyDataSetChanged()입니다. notifyDataSetChanged()의 경우 쓰기는 간편하지만 성능에 좋지 않은 영향을 미치게 됩니다.
// RecyclerView.java
public final void notifyDataSetChanged() {
mObservable.notifyChanged();
}
// RecyclerView.java
public void notifyChanged() {
// since onChanged() is implemented by the app, it could do anything, including
// removing itself from {@link mObservers} - and that could cause problems if
// an iterator is used on the ArrayList {@link mObservers}.
// to avoid such problems, just march thru the list in the reverse order.
for (int i = mObservers.size() - 1; i >= 0; i--) {
mObservers.get(i).onChanged();
}
}
// RecyclerView.java
// Called by onChanged()
void setDataSetChangedAfterLayout() {
if (mDataSetHasChangedAfterLayout) {
return;
}
mDataSetHasChangedAfterLayout = true;
final int childCount = mChildHelper.getUnfilteredChildCount();
for (int i = 0; i < childCount; i++) {
final ViewHolder holder = getChildViewHolderInt(mChildHelper.getUnfilteredChildAt(i));
if (holder != null && !holder.shouldIgnore()) {
holder.addFlags(ViewHolder.FLAG_ADAPTER_POSITION_UNKNOWN);
}
}
mRecycler.setAdapterPositionsAsUnknown();
// immediately mark all views as invalid, so prefetched views can be
// differentiated from views bound to previous data set - both in children, and cache
markKnownViewsInvalid();
}
현재 adapter를 가지고 있는 모든 RecyclerView에게 전체 리스트의 데이터가 변경되었다고 알리고 모든 아이템들을 다시 binding하게 됩니다.
즉, 리스트의 데이터 중 일부만 변경되어서 반영해야할 경우 notifyDataSetChanged를 사용하게 되면 필요 이상의 성능을 낭비하게 됩니다. 이런 일을 방지하기 위해서는 notifyDataSetChanged()가 아닌 변경된 데이터만 update 시켜주는 notifyItemChanged(position), notifyItemInserted(position) 등을 이용하여야 합니다.
하지만, 이전 리스트와 새로운 리스트를 비교하여 알맞은 notify method를 호출하는 것을 구현하기는 쉬운일이 아닙니다.
DiffUtil is a utility class that calculates the difference between two lists and outputs a list of update operations that converts the first list into the second one. [1]
DiffUtil은 Eugene W. Myers의 linear space refinement를 적용한 difference algorithm을 이용하여 두 리스트의 차이와 이전 리스트에서 새로운 리스트로 변환시키는 update 연산을 계산합니다.
DiffUtil.calculateDiff(callback) method를 호출하면 DiffUtil.Result를 반환하며 Result클래스에는 RecyclerView를 결과에 맞게 update시켜주는 dispatchUpdatesTo(adapter) method가 존재합니다. 이를 이용하여 간단하게 두 리스트의 차이를 계산하여 RecyclerView에 반영시킬 수 있습니다.
DiffUtil.calculateDiff(callback)에서 사용되는 callback 클래스입니다. List indexing과 item diffing을 담당하며 사용하기 위해서는 4개의 abstract method들을 구현해 주어야 합니다.
int getOldListSize()
이전 리스트의 크기를 반환 합니다.
int getNewListSize()
새로운 리스트의 크기를 반환 합니다.
boolean areItemsTheSame(int oldItemPosition, int newItemPosition)
oldItemPosition과 newItemPosition에 위치하는 두 아이템이 같은 아이템인지 반환합니다. 두 아이템을 구분하는 UID와 같은 아이템을 구분할 수 있는 property가 존재할 경우 해당 id가 같은지 비교하면 됩니다.
boolean areContentsTheSame(int oldItemPosition, int newItemPosition)
areItemsTheSame가 true를 반환할 경우에만 호출되며 oldItemPosition과 newItemPosition에 위치하는 두 아이템이 같은 내용을 가지고 있는지 반환합니다.
DiffUtil.ItemCallback는 Callback클래스와 달리 item diffing만 담당합니다. 그러므로 ItemCallback을 이용하여 Callback은 list indexing만 담당하도록 관심사를 분리 시킬 수 있습니다.
boolean areItemsTheSame(@NonNull T oldItem, @NonNull T newItem)
Parameter로 받은 두 아이템이 같은지 아이템인지 확인합니다. 여기서도 마찬가지로 UID와 같은 아이템의 unique property가 존재한다면 비교해주도록 구현하면 됩니다.
boolean areContentsTheSame(@NonNull T oldItem, @NonNull T newItem)
areItemsTheSame이 true를 반환한 경우만 호출되며 parameter로 받은 두 아이템이 같은 내용을 가지고 있는지 확인합니다.
두 리스트가 너무 클 경우, 차이를 계산하는 것이 오래 걸릴수 있습니다. 이런 경우 main thread에서 계산을 하고 update를 시키면 사용자가 어플리케이션을 사용하다 잠시 멈춘 것 처럼 보이게 됩니다.
이러한 일을 방지하기 위해서는 두 리스트의 차이는 background thread에서 계산 후 RecyclerView를 update시켜주어야 합니다. 다음 글에서는 이를 가능하게 해주는 AsyncListDiffer와 wrapper 클래스인 ListAdapter가 어떻게 동작하는지 작성할 예정입니다.
[1] "DiffUtil," Android Developers, last modified Feb 24, 2021, accessed Apr 26, 2022, https://developer.android.com/reference/androidx/recyclerview/widget/DiffUtil.