성장기록지
DiffUtil 알아보기 본문
DiffUtil이란
androidx 패키지에 포함되어 두 리스트 간의 차이를 계산하고, 새로운 리스트로 변경하기 위한 작업목록을 반영하는 것에 도움을 주는 유틸리티 클래스이다.
DiffUtil을 쓰지 않았을 때
개인 프로젝트에서 RecyclerView.Adapter 의 list를 update할 때 다음과 같이 진행하였다.코드와 같이 아이템을 추가할 때, 전체를 새로고침하는 notifyDataSetChanged()를 사용하거나.
아니면 최적화를 위해 범위 내에서만 아이템을 삽입하는 notifyItemRangeInserted()를 사용할 수 있다.
class HomeShoppingItemAdapter : RecyclerView.Adapter<HomeShoppingItemAdapter.ShoppingItemViewHolder>() {
private val items = mutableListOf<ShoppingItem>()
//...
fun addItems(shoppingItems: List<ShoppingItem>) {
val startPosition = items.size
items.addAll(shoppingItems)
notifyItemRangeInserted(startPosition, shoppingItems.size)
}
//...
}
하지만 기본적으로 notifyDataSetChanged()는 보이는 모든 View 에 대해서 재배치와 재구성을 진행하게 되어
매우 비효율적이므로 아래의 경고와 같이 최후의 보루로 사용하라고 경고가 뜬다.
그렇기에 notifyItemRangeInserted() 와 같은 구체적인 notify 함수를 사용해야 하는데,
RecyclerView마다 알맞은 notify 함수를 적용하는 것은 무척 번거로운 일이다.
DiffUtil과 AsyncListDiffer
위와 같은 번거로움을 해결하기 위해 DiffUtil이 나오게 되었다.
DiffUtil AsyncListDiffer 를 통해 이용할 수 있다.
아래는 AsyncListDiffer의 설명이다.
문서의 내용과 같이 AsyncListDiffer 는 새 리스트를 수신받으면, background thread 에서 새 리스트와 기존 리스트 간의 차이점을 계산하고, 연결된 RecyclerView.Adapter 에 어떻게 리스트를 변경하면 되는지 신호를 수신하는 역할을 한다.
AsnycListDiffer 구조를 보면 AsyncListDiffer 는 RecyclerView.Adapter 와 DiffUtil.ItemCallback 를 포함하고 있는 것을 확인할 수 있다.
구조만 보면 우리가 정의한 DiffUtil.ItemCallback 의 내용을 참고하여 두 리스트 간의 차이점을 계산하고, 그 결과를 RecyclerView.Adapter 에 전달하는 작업이 진행될 것으로 추측된다.
AsyncListDiffer의 내부코드를 살펴보고자 한다.
하지만 내용이 워낙 방대하여 이해하기 쉽지않으니
아래의 요약내용을 이해하고 코드는 참고로 보면 좋다.
(필자도 완벽히 이해하지 못하였다.)
AsyncListDiffer 의 submitList() 는 새로운 List 가 들어왔을 때 DiffUtil.Callback 구현부를 참고하며,
두 리스트 간의 차이점을 얻어내어 DiffResult 를 반환하고,
반환받은 DiffResult 값을 기반으로 AsyncListDiffer 내에서
RecyclerView.Adapter 의 notifyItemRangeInserted(), notifyItemRangeRemoved() 등의
함수를 실행하도록 구현되어 있는 것으로 정리할 수 있다.
AsyncListDiffer 는 이와 같은 방법으로 개발자들이 직접 notifyItem 함수 사용에 대한 번거로움을
해소한 것을 알 수 있다.
이제 AsyncListDiffer의 내부코드를 살펴보자.
우선 RecyclerView.Adapter 에 포함하기 위한 itemList (mList, mReadOnlyList) 를 가지고 있다.
getCurrentList() 를 통해 이를 반환받을 수 있다.
그리고 submitList() 를 통해 backgroundThreadExecutor 에서 DiffUtil.calculateDiff() 함수를 호출하여
두 리스트 간의 차이점을 얻어내고, mainThreadExecutor 에서 latchList() 를 통해
새로운 List 를 업데이트하도록 구현이 되어 있는 것을 볼 수 있다.
(코드가 너무 길어서 요약하여 가져왔다.)
@SuppressWarnings("WeakerAccess")
public void submitList(@Nullable final List<T> newList) {
submitList(newList, null);
}
@SuppressWarnings("WeakerAccess")
public void submitList(@Nullable final List<T> newList,
@Nullable final Runnable commitCallback) {
...
final List<T> oldList = mList;
mConfig.getBackgroundThreadExecutor().execute(new Runnable() {
@Override
public void run() {
final DiffUtil.DiffResult result = DiffUtil.calculateDiff(new DiffUtil.Callback() {
@Override
public int getOldListSize() {
return oldList.size();
}
@Override
public int getNewListSize() {
return newList.size();
}
@Override
public boolean areItemsTheSame(int oldItemPosition, int newItemPosition) {
...
}
@Override
public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) {
...
}
@Nullable
@Override
public Object getChangePayload(int oldItemPosition, int newItemPosition) {
...
}
});
mMainThreadExecutor.execute(new Runnable() {
@Override
public void run() {
if (mMaxScheduledGeneration == runGeneration) {
latchList(newList, result, commitCallback);
}
}
});
}
});
}
void latchList(
@NonNull List<T> newList,
@NonNull DiffUtil.DiffResult diffResult,
@Nullable Runnable commitCallback) {
final List<T> previousList = mReadOnlyList;
mList = newList;
// notify last, after list is updated
mReadOnlyList = Collections.unmodifiableList(newList);
diffResult.dispatchUpdatesTo(mUpdateCallback);
onCurrentListChanged(previousList, commitCallback);
}
그리고 latchList 내부의 diffResult.dispatchUpdatesTo(mUpdateCallback) 를 통해
DiffResult 결과에 따라서
updateCallback(혹은 BatchingCallback 의) onInsert(), onRemoved(), onMoved(), onChanged()
함수가 실행되는 것을 볼 수 있다.
// DiffUtil.java
public void dispatchUpdatesTo(@NonNull ListUpdateCallback updateCallback) {
final BatchingListUpdateCallback batchingCallback;
if (updateCallback instanceof BatchingListUpdateCallback) {
batchingCallback = (BatchingListUpdateCallback) updateCallback;
} else {
batchingCallback = new BatchingListUpdateCallback(updateCallback);
// replace updateCallback with a batching callback and override references to
// updateCallback so that we don't call it directly by mistake
//noinspection UnusedAssignment
updateCallback = batchingCallback;
}
...
for (int diagonalIndex = mDiagonals.size() - 1; diagonalIndex >= 0; diagonalIndex--) {
...
while (posX > endX) {
// REMOVAL
...
if ((status & FLAG_MOVED) != 0) {
...
if (postponedUpdate != null) {
...
batchingCallback.onMoved(posX, updatedNewPos - 1);
if ((status & FLAG_MOVED_CHANGED) != 0) {
...
batchingCallback.onChanged(updatedNewPos - 1, 1, changePayload);
}
} else {
...
}
} else {
// simple removal
batchingCallback.onRemoved(posX, 1);
currentListSize--;
}
}
while (posY > endY) {
// ADDITION
...
if ((status & FLAG_MOVED) != 0) {
...
if (postponedUpdate == null) {
...
} else {
...
batchingCallback.onMoved(updatedOldPos, posX);
if ((status & FLAG_MOVED_CHANGED) != 0) {
...
batchingCallback.onChanged(posX, 1, changePayload);
}
}
} else {
// simple addition
batchingCallback.onInserted(posX, 1);
currentListSize++;
}
}
...
for (int i = 0; i < diagonal.size; i++) {
// dispatch changes
if ((mOldItemStatuses[posX] & FLAG_MASK) == FLAG_CHANGED) {
...
batchingCallback.onChanged(posX, 1, changePayload);
}
...
}
...
}
batchingCallback.dispatchLastEvent();
이때 인자로 전달되었던 updateCallback 은 AsyncListDiffer 의 mUpdateCallback 값 이다.
mUpdateCallback 는 AsyncListDiffer 생성자 시점에 만들어지는 AdapterListUpdateCallback 이며,
내부적으로 onInserted() onRemoved() 등이 실행되었을 때 각각 RecyclerView.Adapter.notifyItemRangeInserted(), RecyclerView.Adapter.notifyItemRangeRemoved() 등이 실행되도록 구현되어 있다.
ListAdapter
androidx 는 AsyncListDiffer 를 더욱 편리하게 개발할 수 있도록 ListAdapter 라는 추상클래스를 제공하고 있다.
코드를 통해 어떻게 구현되어있는지 살펴보자.
우선 ListAdapter 에서는 AsyncListDiffer 를 포함하고 있다.
그리고 AsyncListDiffer 를 이용하여 RecyclerView.Adapter 로 인하여 반드시 구현해주어야 하는 getItemCount() 를 대신 구현해 주었으며, 그 외 자주 구현하는 getItem(), getCurrentList(), submitList() 들이 함께 구현되어있다.
'안드로이드 > 안드로이드 지식' 카테고리의 다른 글
StateFlow 개념과 사용 이유 (0) | 2025.01.26 |
---|---|
Kotlin Flow란? (0) | 2025.01.24 |
Kotlin Data Stream (Sequence, Hot, Cold Stream) (0) | 2025.01.23 |
Compose Structure 정리하기 (compiler, Runtime, UI) (0) | 2025.01.22 |
Compose Stability 자세히 알아보기! (0) | 2025.01.21 |