[Android] ListAdapter

HEETAE HEO·2022년 4월 26일
0

Android

목록 보기
1/12
post-thumbnail

Adapter의 역할

  1. RecyclerView에 보여줄 데이터 리스트 관리

  2. View 객체를 재사용하기 위한 ViewHolder 객체 생성

  3. 데이터 리스트에서 position에 해당되는 데이터를 itemView에 표시

RecyclerView에서 ListAdapter를 사용하는 이유는 대부분의 경우 RecyclerView의 데이터는 동적으로 변경됩니다.

그러면 왜 ListAdapter를 사용할까?

이에 대한 설명은 예시로 설명하겠습니다.

예시

메모 작업의 동작으로 설명을 한다면 새 작업을 추가하고 삭제하는 과정을 실행한다면 목록에서 새로운 작업들이 추가되고 삭제되어야한다. 데이터를 지정된 위치에 저장해주는 notifyiteminserted() 메서드를 사용하여 추가를 해 줄 수 있지만 문제는 요소의 삭제에서 문제가 된다. 코드가 매우 복잡하고 까다로워 많은 사람들은 notifyDataSetChanged()를 호출한다. 하지만 이 방법은 데이터를 변경되지 않은 부분을 포함하여 전체를 다시 그리는 동작을 하기 때문에 데이터가 자주 바뀌는 환경에서는 비용이 많이 든다는 단점이 있다.

이러한 문제를 해결할 수 있는 것이 ListAdapter이다. ListAdapter를 사용하면 요소의 추가 및 삭제를 처리할 수 있으며 변경사항에 애니메이션 효과를 추가할 수 도 있다.

코드로 알아보는 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();
}

다음은 notifyDataSetChanged()의 자바 동작코드이다.

위의 코드를 보면 현재 adapter를 가지고 있는 모든 RecyclerView에게 전체 리스트의 데이터가 변경되었다고 알리고 모든 아이템들을 다시 Binding하게 됩니다.

즉, 리스트의 데이터 중 일부만 변경되어서 반영해야할 경우 notifyDataSetChanged를 사용하게 되면 필요 이상의 성능을 낭비하게 되는 것이다.

그렇기에 우리가 사용을 해야할 것은 바로 아래의 ListAdapter이다.

ListAdapter

ListAdapter의 가장 대표적인 기능이 DiffUtil이다.

DiffUtil메서드의 역할은

  1. calculateDiff()에서 diff알고리즘을 통해 변경된 아이템을 감지하고,

  2. dispatchUpdatesTo()에서 지정된 Adapter로 업데이트 이벤트를 전달한다.

calculateDiff()는 RecyclerView와 함께 사용할 수 있는 메서드로 두 데이터 세트 간의 차이점을 계산하여 RecyclerView의 새로 고침을 최적화 합니다.

dispatchUpdatesTo

public void dispatchUpdateTo(Adapter adapter)

List oldList = mAdapter.getData();
DiffResult result = DiffUtil.calculateDiff(new MyCallback(oldList, newList));
mAdapter.setData(newList);
result.dispatchUpdatesTo(mAdapter);

dispatchUpdatesTo() 메서드는 DiffUtil.DiffResult 객체를 매개변수로 받습니다. 이 객체는 이전 데이터 세트와 새 데이터 세트 간의 차이점을 나타내며, RecyclerView를 업데이트 하는데 필요한 최소한의 작업을 수행합니다.

dispatchUpdatesTo() 메서드는 새로 추가된 항목, 변경된 항목, 제거된 항목을 RecyclerView.Adapter에 알려주고 Adapter는 이러한 변경 사항을 RecyclerView에 반영하여 View를 업데이트 합니다.

DiffUtil vs AsyncListDiffer

AsyncListDiffer를 사용하는 것과 DiffUtil 클래스를 직접 사용하는 것의 차이점은 다음과 같습니다.

  1. 비동기 처리: AsyncListDiffer는 백그라운드 스레드에서 데이터 변경을 처리하여 UI 스레드가 차단되지 않도록 합니다. DiffUtil 클래스를 직접 사용하는 경우에는 UI 스레드에서 작업을 수행하므로, 큰 데이터 세트에서는 성능 문제가 발생할 수 있습니다.

  2. 자동 업데이트: AsyncListDiffer는 데이터 변경을 자동으로 처리합니다. 새로운 데이터 세트가 제공되면, AsyncListDiff는 이전 데이터 세트와 비교하여 변경 내용을 계산하고 RecyclerView를 업데이트 합니다. DiffUtil 클래스를 직접 사용하는 경우에는 변경 내용을 수동으로 처리해야 합니다.

  3. 콜백 처리: AsyncListDiffer는 DiffUtil.ItemCallback 클래스를 사용하여 두 데이터 항목 간의 차이점을 계산합니다. DiffUtil 클래스를 직접 사용하는 경우에는 이 콜백 처리를 직접 구현해야합니다.

다음의 차이를 코드로 보겠습니다.

public class MyAdapter extends ListAdapter<MyData, MyViewHolder> {

    private AsyncListDiffer<MyData> mDiffer;

    public MyAdapter() {
        super(new DiffUtil.ItemCallback<MyData>() {
            @Override
            public boolean areItemsTheSame(@NonNull MyData oldItem, @NonNull MyData newItem) {
                return oldItem.getId() == newItem.getId();
            }

            @Override
            public boolean areContentsTheSame(@NonNull MyData oldItem, @NonNull MyData newItem) {
                return oldItem.equals(newItem);
            }
        });

        mDiffer = new AsyncListDiffer<>(this, new DiffUtil.ItemCallback<MyData>() {
            @Override
            public boolean areItemsTheSame(@NonNull MyData oldItem, @NonNull MyData newItem) {
                return oldItem.getId() == newItem.getId();
            }

            @Override
            public boolean areContentsTheSame(@NonNull MyData oldItem, @NonNull MyData newItem) {
                return oldItem.equals(newItem);
            }
        });
    }

    public void setData(List<MyData> newData) {
        mDiffer.submitList(newData);
    }

    @NonNull
    @Override
    public MyViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
        // View holder 생성
    }

    @Override
    public void onBindViewHolder(@NonNull MyViewHolder holder, int position) {
        // View holder 바인딩
    }
}

위 코드에서 AsyncListDiffer 객체를 생성하여 RecyclerView.Adapter의 인스턴스를 전달하고, 데이터 변경을 처리하는 DiffUtil.ItemCallback 객체를 전달합니다. setData() 메서드에서는 데이터 세트를 제출하고, RecycelerView.Adapter에서 필요한 onCreateViewHolder()와 onBindViewHolder()메서드를 오버라이드 하여 ViewHolder를 생성하고 데이터를 바인딩 합니다.

AsyncListDiffer는 내부적으로 데이터 변경을 비교하고 RecyclerView를 업데이트하는 것을 처리하므로, RecyclerView.Adapter에서는 이를 별도로 구현할 필요가 없습니다. 또한, 데이터 변경을 처리하는 동안 UI 스레드가 차단되지 않도록 하기 위해 백그라운드 스레드에서 작업을 수행합니다.

public class MyAdapter extends RecyclerView.Adapter<MyViewHolder> {

    private List<MyData> mData;

    public MyAdapter(List<MyData> data) {
        mData = data;
    }

    public void setData(List<MyData> newData) {
        List<MyData> oldData = mData;
        mData = newData;
        DiffUtil.DiffResult diffResult = DiffUtil.calculateDiff(new MyCallback(oldData, newData));
        diffResult.dispatchUpdatesTo(this);
    }

    @NonNull
    @Override
    public MyViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
        // View holder 생성
    }

    @Override
    public void onBindViewHolder(@NonNull MyViewHolder holder, int position) {
        // View holder 바인딩
    }

    @Override
    public int getItemCount() {
        return mData.size();
    }

    private static class MyCallback extends DiffUtil.Callback {

        private List<MyData> mOldData;
        private List<MyData> mNewData;

        public MyCallback(List<MyData> oldData, List<MyData> newData) {
            mOldData = oldData;
            mNewData = newData;
        }

        @Override
        public int getOldListSize() {
            return mOldData.size();
        }

        @Override
        public int getNewListSize() {
            return mNewData.size();
        }

        @Override
        public boolean areItemsTheSame(int oldItemPosition, int newItemPosition) {
            MyData oldItem = mOldData.get(oldItemPosition);
            MyData newItem = mNewData.get(newItemPosition);
            return oldItem.getId() == newItem.getId();
        }

        @Override
        public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) {
            MyData oldItem = mOldData.get(oldItemPosition);
            MyData newItem = mNewData.get(newItemPosition);
            return oldItem.equals(newItem);
        }
    }
}

위의 코드는 DiffUtil 클래스를 직접 사용하는 코드입니다.

setData() 메서드에서 DiffUtil.calculateDiff()를 사용하여 이전 데이터 세트와 새로운 데이터 세트 간의 차이점을 계산합니다. 이후 DiffUtil.DiffResult 객체의 dispatchUpdatesTo() 메서드를 사용하여 RecyclerView를 업데이트합니다. 이 과정에서 UI 스레드가 차단될 수 있으므로, 큰 데이터 세트에서는 성능 문제가 발생할 수 있습니다.

DiffUtil.Callback 구현

class ModelDiffItemCallback : DiffUtil.ItemCallback<Model>(){
            override fun areItemsTheSame(oldItem: Model, newItem: Model) =
            oldItem.id == newItem.id

            override fun areContentsTheSame(oldItem: Model, newItem: Model)=
            oldItem == newItem

      
    }

DiffUtil.ItemCallback는 Callback 클래스와 달리 item diffing만을 담당한다. 그러므로 ItemCallback을 이용하여 Callback은 list index만 담당하도록 관심사를 분리 시킬 수 있다.

  1. boolean areItemsTheSame(@NonNull T oldItem, @NonNull T newItem)
    : 두 아이템이 같은 아이템인지 확인을 한다. 각 아이템의 고유 키를 비교하여 같은지를 확인 같다면 True 다르다면 False를 반환한다. True 이면 다음 메서드인 areContentsTheSame을 동작한다. False라면 ViewHolder자체를 새로 만든다.

  1. boolean areContentsTheSame(@NonNull T oldItem, @NonNull T newItem)
    : areContentsTheSame의 경우 areItemTheSame이 True값을 반환해야 동작을 한다.
    아이템의 모든 값들을 비교해 데이터 값의 차이가 있다면 해당 테이터 값만을 변경해준다.

그리고 RecyclerView Adapter에서 AsyncListDiffer를 생성해 아래처럼 사용하면 된다.

class ModelAsyncDifferAdapter : RecyclerView.Adapter<PersonViewHolder>() {
    private val asyncDiffer = AsyncListDiffer(this, ModelDiffItemCallback())

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = PersonViewHolder(
        ItemModelBinding.inflate(LayoutInflater.from(parent.context), parent, false)
    )

    override fun onBindViewHolder(holder: ModelViewHolder, position: Int) =
        holder.bind(asyncDiffer.currentList[position])

    override fun getItemCount() = asyncDiffer.currentList.size

    fun replaceItems(newModel: List<Model>) {
        asyncDiffer.submitList(newModel)
    }
}

두 리스트의 index의 값이 크다면 차이를 계산하는 과정에서 main thread에서 계산을 하고 update를 시키면 애플리케이션을 사용하다 잠시 멈춘 것 처럼보이게 되는데 이러한 현상을 위의 코드에서 처럼 AsyncListDiffer을 통해 background thread에서 계산을 처리하고 RecyclerView를 update해주기 때문에 원활한 앱 사용을 도와준다

마치며

ListAdapter를 사용하면 변경 된 아이템만 UI를 업데이트 해주고 boiler plate코드도 엄청나게 줄여주는 효과, 스레드 및 애니메이션 동작까지 알아서 다 해준다. 그렇기에 ListAdapter는 데이터가 동적으로 계속 바뀌는 배달 어플의 주문 수, 리뷰 수, 찜의 수 와같이 사용자에게 변동되는 데이터를 즉각적으로 보여주고자 할 때 효율적으로 사용이 된다.

profile
Android 개발 잘하고 싶어요!!!

0개의 댓글