ListAdapter 파헤치기

berry·2023년 11월 2일
0

ListAdapter ? 그게 뭐야?

RecyclerView 의 Adapter 를 구현할 , RecyclerView.Adapter 를 상속받아 구현하는 방법보다 ListAdapter 를 상속하는 방식이 더 빠르다고 알려져있다.

ListAdapter 는 AsyncListDiffer 를 사용하고 있고, AsyncListDiffer 가 내부적으로 DiffUtil.ItemCallback 을 사용하고 있다.
ListAdapter-> AsyncListDiffer -> DiffUtil

DiffUtil 클래스는 이전 데이터 상태와 현재 데이터간의 상태 차이를 계산하고, 반드시 업데이트해야 할 최소한의 데이터에 대해서만 갱신하는 클래스이다.
DiffUitl 이 원래 목록과 새로 들어온 목록간의 차이를 계산하고 난 뒤 DiffUtil.Callback 이라는 추상 클래스를 콜백 클래스로 활용하게 된다. DiffUtil.Callback 은 4개의 추상 메소드와 1개의 일반 메소드로 이루어져있는데, 이러한 메소드를 오버라이딩하여 사용한다. (4개 추상 메소드의 경우 당연하게도 오버라이딩 필수)

기존의 RecyclerView 를 사용할 때 동적으로 데이터가 변경되는 경우 RecyclerView Adapter 가 제공하는notifyItem 메소드 를 이용해 아이템의 갱신을 리사이클러뷰에 알려줬었다.

그런데 개발자가 데이터가 변경되는 방식을 직접 판단하고, 그때마다 이렇게 notify를 일일이 해 주는것은 번거롭다. 그리고 알맞지 않은 메소드를 사용한다면 갱신이 필요없는 ViewHolder를 같이 갱신하는 불필요한 작업이 생길수도 있다.

  • notifyItemChanged(int)
  • notifyItemInserted(int)
  • notifyItemRemoved(int)
  • notifyItemRangeChanged(int, int)
  • notifyItemRangeInserted(int, int)
  • notifyItemRangeRemoved(int, int)
  • ..

DiffUtil 클래스는 두 데이터셋을 받아서 그 차이를 계산해주는 클래스이다. DiffUtil을 사용하면 두 데이터 셋을 비교한 뒤 그중 변한부분만을 파악하여 Recyclerview에 반영할 수 있다.
DiffUtil 함수의 코드를 까보면 내부적으로 데이터 변경 상황에 맞게 아래와 같은 함수들 중 적절한 하나를 알아서 실행시켜주고 있다.
DiffUtil 이 내부적으로 알아서 처리해주기 때문에 더 이상 위의 번거롭고 실수가 발생할 수 있는 과정을 거치지 않아도 된다. 캡슐화 ~ 👍

ListAdapter 코드 까보기

사전 지식

Main Thread

  • 주로 UI 작업, UI 스레드라고도 명칭.
  • Main Thread에서 UI 작업이 아닌 시간이 오래 걸리는 작업을 하게 되면 ANR이 발생할 수도 있음.
  • 안드로이드 컴포넌트의 생명주기 메소드와 그 내부 호출은 모두 Main Thread에서 처리
  • Activity외에도 BroadcastReceiver, Service, Application 이 UI와 관련이 없더라도 Main Thread에서 작업 처리

일반 Thread

  • 애플리케이션의 성능을 향상시키기 위한 Thread 생성, Network 작업, DB 쿼리 등 오래 걸리는 작업들을 주로 처리.
  • 일반 Thread는 백그라운드에서 처리하는 작업들이 많아 Background 스레드라고도 부른다.

Looper 란

  • Looper는 Message Queue 를 관리하는 클래스로, Message나 Runnable 객체를 하나씩 꺼내서 Handler에 전달한다.
  • Main Thread에는 기본적으로 실행 시에 Looper를 생성

Handeler 란

  • Handler는 Looper로 부터 처리되어야 할 Message들을 받아서 처리하는 역할과, 다시 Message Queue에 Message를 전달하는 역할을 한다. - 또한, Thread간의 통신을 담당한다고 할 수 있다.

Handler 의 주요 용도
1. 일반 스레드에서 UI 업데이트
일반 스레드에서 네트워크 통신이나 DB 작업 시 UI 업데이트가 필요할 경우 Main Thread의 Handler를 통해 업데이트 작업을 Runnable이나 Message를 통해 요청할 수 있음
2. 메인 스레드에서 다음 작업 예약
메인 스레드에서 UI 작업을 바로 하지 못하는 경우도 있다. 이 때 API 중 Delayed가 붙은 메소드를 통해 특정 시간 이후에 실행할 수 있도록 Handler를 사용할 수 있음
3. 반복 갱신
4. 시간 제한

더 자세히 알고 싶다면 아래 블로그 방문 ~
https://brunch.co.kr/@mystoryg/84

ListAdapter.java

ListAdapter 는 2가지 생성자를 제공하고 있다.

  • DiffUtil.ItemCallback 객체 주입
  • AsyncDifferConfig 객체 주입
    - AsyncDifferConfig: 내부적으로 main thread executor 와 background thread executor 를 별도로 설정하고 싶을 때 등록할 수 있게 하는 API 를 제공하는 객체
    • 특별히 메인 스레드나 작업 스레드 환경을 지정하고자 하는게 아니라면 사용할 필요 없음
public abstract class ListAdapter<T, VH extends RecyclerView.ViewHolder>
        extends RecyclerView.Adapter<VH> {
    final AsyncListDiffer<T> mDiffer;
 
    protected ListAdapter(@NonNull DiffUtil.ItemCallback<T> diffCallback) {
        mDiffer = new AsyncListDiffer<>(new AdapterListUpdateCallback(this),
                new AsyncDifferConfig.Builder<>(diffCallback).build());
        mDiffer.addListListener(mListener);
    }
 
    protected ListAdapter(@NonNull AsyncDifferConfig<T> config) {
        mDiffer = new AsyncListDiffer<>(new AdapterListUpdateCallback(this), config);
        mDiffer.addListListener(mListener);
    }
    
    // ...
}

ListAdapter 는 AsyncListDiffer 를 프로퍼티로 가지며, 실제 ItemList 객체 관리나 DiffUtil 을 통한 Item 변경 사항 확인 및 로직 처리 등은 AsyncListDiffer 클래스 내에서 이루어진다.

ListAdapter 클래스 안에는 submitList 함수가 오버로딩 되어있다.

public void submitList(@Nullable List<T> list)
  • 차이점을 비교하고 표시할 새 List 를 제출한다.
  • List 가 이미 화면에 보여지고 있는 경우 background thread 에서 차이점이 계산되어 main thread 에서 Adapter.notifyItem 이벤트를 디스패치한다.
public void submitList(@Nullable List<T> list, @Nullable final Runnable commitCallback)
  • 위의 submitList() 와 같은 역할을 하는 함수이지만, 차이점이 있다면 onCurrentListChanged() 함수가 호출될 때 실행되는 콜백을 넘길 수 있다.
  • Runnable 은 List가 커밋될 때 실행되는 선택적 실행 가능 항목(커밋된 경우)이다.

AsyncListDiffer 코드 까보기

AsyncListDiffer.java

 public void submitList(@Nullable final List<T> newList,
            @Nullable final Runnable commitCallback) {
        // incrementing generation means any currently-running diffs are discarded when they finish
        final int runGeneration = ++mMaxScheduledGeneration;

        if (newList == mList) {
            // nothing to do (Note - still had to inc generation, since may have ongoing work)
            if (commitCallback != null) {
                commitCallback.run();
            }
            return;
        }

        final List<T> previousList = mReadOnlyList;

        // fast simple remove all
        if (newList == null) {
            //noinspection ConstantConditions
            int countRemoved = mList.size();
            mList = null;
            mReadOnlyList = Collections.emptyList();
            // notify last, after list is updated
            mUpdateCallback.onRemoved(0, countRemoved);
            onCurrentListChanged(previousList, commitCallback);
            return;
        }

        // fast simple first insert
        if (mList == null) {
            mList = newList;
            mReadOnlyList = Collections.unmodifiableList(newList);
            // notify last, after list is updated
            mUpdateCallback.onInserted(0, newList.size());
            onCurrentListChanged(previousList, commitCallback);
            return;
        }

        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);
                        }
                    }
                });
            }
        });
    }

AsyncListDiffer.submitList() 가 호출되면 기존 리스트를 previousList 에 보관하고 새로운 리스트로 currentList 를 업데이트 한다.
이때 아이템에 변동이 생기면 내부적으로 콜백리스너를 통해 RecyclerView.Adapter 의 함수들이 호출된다. (notifyItemRangeInserted(), notifyItemRangeRemoved() 등등)

그리고 background thread 에서 DiffUtil.calculateDiff() 함수가 호출되는데, 이때 이 함수의 파라미터로 우리가 ListAdapter 의 생성자로 넘긴 DiffUtil.ItemCallback 이 활용된다.
내부적으로 아이템들을 비교해나가기 시작하는데 이때 우리가 오버라이딩한 areItemsTheSame(), areContentsTheSame() 함수가 사용된다.

background thread 에서 기존 리스트와 새 리스트의 차이점을 비교하는 작업을 하고 MainThreadExecutor 는 Handler 을 이용해 main thread(ui thread) 에서 callback 함수를 실행한다.

profile
공부 내용 기록

0개의 댓글