성장기록지

DiffUtil 알아보기 본문

안드로이드/안드로이드 지식

DiffUtil 알아보기

pengcon 2025. 1. 30. 18:04

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의 내부코드를 살펴보고자 한다.

하지만 내용이 워낙 방대하여 이해하기 쉽지않으니

아래의 요약내용을 이해하고 코드는 참고로 보면 좋다.

(필자도 완벽히 이해하지 못하였다.)

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() 들이 함께 구현되어있다.