본문 바로가기

안드로이드

[안드로이드] ListAdapter

ListAdapter에 대해 알아보자.

 

ListAdapter를 간단하게 이해한 바로는
Recyclerview를 사용하면서 데이터를 더 쉽고 효율적으로 관리할 수 있도록 구현된 Adapter이다.

 

ListAdapter를 사용하게 된 배경은 크게 아래와 같다.

 

  1. 기본 RecyclerView.Adapter를 사용하면서 데이터를 변경하고 이를 알리기 위해 notify 함수들을 호출한다
  2. 효율적인 사용을 위해 현재 데이터와 새 데이터를 비교해서 필요한 데이터만 바꿔주는 RecyclerView.DiffUtil 클래스를 사용한다
  3. DiffUtil 비교 연산을 background thread에서 실행하기 위해 AsyncListDiffer 클래스를 Adapter 내에 구현한다
  4. AsyncListDiffer의 기능을 wrapping 하여 더 편리하게 사용할 수 있는 ListAdapter를 사용한다

 

먼저, 기본 RecyclerView.Adapter를 사용하는 경우를 알아보자.

 

ListAdapter를 사용하지 않으면서 Recyclerview를 사용할 때에는

데이터를 업데이트하기 위해 아래와 같은 notify 함수들을 사용하게 된다.

 

public final void notifyDataSetChanged() {
    mObservable.notifyChanged();
}

public final void notifyItemChanged(int position) {
    mObservable.notifyItemRangeChanged(position, 1);
}

public final void notifyItemChanged(int position, @Nullable Object payload) {
    mObservable.notifyItemRangeChanged(position, 1, payload);
}

public final void notifyItemRangeChanged(int positionStart, int itemCount) {
    mObservable.notifyItemRangeChanged(positionStart, itemCount);
}

public final void notifyItemRangeChanged(int positionStart, int itemCount,
        @Nullable Object payload) {
    mObservable.notifyItemRangeChanged(positionStart, itemCount, payload);
}

public final void notifyItemInserted(int position) {
    mObservable.notifyItemRangeInserted(position, 1);
}

public final void notifyItemMoved(int fromPosition, int toPosition) {
    mObservable.notifyItemMoved(fromPosition, toPosition);
}

public final void notifyItemRangeInserted(int positionStart, int itemCount) {
    mObservable.notifyItemRangeInserted(positionStart, itemCount);
}

public final void notifyItemRemoved(int position) {
    mObservable.notifyItemRangeRemoved(position, 1);
}

public final void notifyItemRangeRemoved(int positionStart, int itemCount) {
    mObservable.notifyItemRangeRemoved(positionStart, itemCount);
}

 

 

처음 리사이클러뷰를 사용할 때에는 notifyDataSetChanged() 메서드를 통해 모든 데이터를 변경했었다.

 

하지만 이 메서드는 리스트의 크기와 데이터가 모두 변경되어 전체 리스트를 다시 그리게 된다.

데이터의 갯수가 많아질수록 비효율적인 처리가 된다.

 

그래서 비효율적인 처리를 줄이기 위해 변경된 데이터만 알리는 메서드를 사용하기 시작했다.

notifyItemInserted(), notifyItemRemoved() ...

 

이 함수들을 사용하면 변경된 부분만 처리할 수 있어 비교적 효율적으로 데이터를 변경할 수 있다.

하지만 항상 리스트의 position을 찾고 직접 업데이트를 해주어야 하기 때문에 약간의 불편함이 있다.

 

이런 불편함을 대체하기 위해 DiffUtil 클래스를 사용해보자.

 

 

DiffUtil


DiffUtil두 개의 리스트의 차이를 계산하고 달라진 데이터만 업데이트하는 Util 클래스이다.

RecyclerView Adapter의 업데이트를 계산하는데 사용할 수 있다.

 

뒤에서 알아볼 내용이지만, background thread에서 DiffUtil의 기능을 쉽게 사용하기 위해 ListAdapter, AsyncListDiffer를 사용할 수 있다.

 

데이터의 양이 적다면 처리를 하는데 많은 시간이 걸리지 않는다.

 

하지만 데이터의 사이즈가 크면 계산하는데 많은 시간이 걸릴 수 있다.

 

그래서 되도록 background thread에서 실행하고 결과값만 main thread에서 처리하는 것을 권장한다.

 

아래는 DiffUtil을 사용해 오버라이드 한 함수들의 예시 코드이다.

 

DiffUtil.Callback은 두 리스트 간의 차이를 계산하는 동안 DiffUtil이 사용하는 클래스이다.

 

class BookDocumentDiffCallback(
    val oldList: List<BookDocument>,
    val newList: List<BookDocument>
) : DiffUtil.Callback() {
    
    // oldList의 size를 리턴
    override fun getOldListSize(): Int = oldList.size

    // newList의 size를 리턴
    override fun getNewListSize(): Int = newList.size

    // DiffUtil에 의해 불리는 함수로 두 객체가 같은 아이템을 나타내는지 결정한다
    // 두 아이템이 같은 객체를 나타내면 true, 다르면 false
    // 고유한 값을 통해 비교한다
    override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean { ... }

    // DiffUtil에 의해 불리는 함수로 두 아이템이 같은 데이터를 갖고 있는지 확인할 때 사용된다
    // DiffUtil은 이 함수로 아이템의 내용이 변경되었는지 감지한다
    // UI에 따라 동작을 변경할 수 있도록 Object.equals(Object) 대신 이 함수를 사용하여 동등성을 확인한다
    // areItemsTheSame 함수가 true를 반환할 때 호출된다
    override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean { ... }
}

 

 

이를 Recyclerview의 Adapter에서 사용할 때에는 아래와 같이 사용할 수 있다.

 

class SearchBookListAdapter : RecyclerView.Adapter<SearchBookViewHolder>() {

    private val bookDocuments = mutableListOf<BookDocument>()

    fun setItems(newBookDocuments: List<BookDocument>) {
        val diffCallback = BookDocumentDiffCallback(bookDocuments, newBookDocuments)
        val diffResult = DiffUtil.calculateDiff(diffCallback)

        bookDocuments.clear()
        bookDocuments.addAll(newBookDocuments)

        diffResult.dispatchUpdatesTo(this)
    }
}

 

 

만들어놓은 DiffCallback에 기존 아이템과 새로운 아이템들을 생성자로 넣어주고 calculateDiff() 함수를 통해 diffResult를 가져온다.

 

calcuateDiff 함수는 DiffUtil 클래스에 있는 함수이다.

 

// 리스트를 다른 리스트로 변환할 수 있는 update 연산을 계산하며 DiffResult를 리턴한다
@NonNull
public static DiffResult calculateDiff(@NonNull Callback cb) {
    return calculateDiff(cb, true);
}

// calculateDiff 함수의 결과에 대한 정보를 가진다
public static class DiffResult { ... }

 

 

이후 mutable하게 만들어놓은 List에 새로운 아이템을 넣어 교체하고

DiffResult의 dispatchUpdatesTo() 함수를 호출한다.

 

dispatchUpdatesTo() 함수를 호출하면 리스트의 갱신이 시작된다.

 

// Adapter에 update event를 전달한다
public void dispatchUpdatesTo(@NonNull final RecyclerView.Adapter adapter) {
    dispatchUpdatesTo(new AdapterListUpdateCallback(adapter));
}

 

 

dispatchUpdatesTo 함수를 호출하기 전에 먼저 mutableList를 통해 새로운 아이템 리스트로 교체하는 이유는, 

위 dispatchUpdatesTo 함수를 보면 새로운 AdapterListUpdateCallback 클래스를 생성해서 arguments에 넣어주는 것을 볼 수 있다.

 

dispatchUpdatesTo 함수 내부에서 데이터가 변경될 때 AdapterListUpdateCallbacknotify 메서드들을 호출하게 된다.

 

AdapterListUpdateCallback은 아래와 같이 구현되어 있다.

 

public final class AdapterListUpdateCallback implements ListUpdateCallback {
    @NonNull
    private final RecyclerView.Adapter mAdapter;

    public AdapterListUpdateCallback(@NonNull RecyclerView.Adapter adapter) {
        mAdapter = adapter;
    }

    @Override
    public void onInserted(int position, int count) {
        mAdapter.notifyItemRangeInserted(position, count);
    }

    @Override
    public void onRemoved(int position, int count) {
        mAdapter.notifyItemRangeRemoved(position, count);
    }

    @Override
    public void onMoved(int fromPosition, int toPosition) {
        mAdapter.notifyItemMoved(fromPosition, toPosition);
    }

    @Override
    public void onChanged(int position, int count, Object payload) {
        mAdapter.notifyItemRangeChanged(position, count, payload);
    }
}

 

즉, notify 함수들을 통해 Adapter에 바로 update event가 전달되기 때문에 dispatchUpdatesTo 함수를 호출하기 전에 RecyclerView의 리스트가 먼저 변경되어 있어야 한다.

 

 

AsyncListDiffer


DiffUtil 클래스를 효율적으로 사용하기 위해서는 백그라운드 스레드에서 비교 처리를 수행하고 결과를 메인 스레드에서 처리하는 코드를 작성해야 한다.

 

DiffUtil을 사용해도 데이터의 양이 많다면 DiffUtil의 연산이 오래 걸릴 수 있기 때문이다.

 

그래서 background thread에서 비교 연산을 수행하고 결과를 main thread에서 처리하는 것을 권장한다.

 

AsyncListDiffer를 사용하면 background thread에서 비교 연산을 수행하고 리스트를 업데이트하는 것 까지 편리하게 할 수 있다.

 

내부적으로 처리하기 때문에 쓰레드를 신경 쓰지 않고 DiffUtil을 훨씬 편하게 사용할 수 있다.

 

class SearchBookListAdapter : RecyclerView.Adapter<SearchBookViewHolder>() {

    private val asyncListDiffer = AsyncListDiffer(this, BookDocumentDiffCallback())

    private val bookDocuments = mutableListOf<BookDocument>()

    fun setItems(newBookDocuments: List<BookDocument>) {
        asyncListDiffer.submitList(newBookDocuments)
    }
}

 

 

AsyncListDiffer 클래스는 생성자의 2번째 argument로 DiffUtil.ItemCallback을 구현한 객체를 받는다.

 

class BookDocumentDiffCallback : DiffUtil.ItemCallback<BookDocument>() {

    override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean { ... }

    override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean { ... }
}

 

 

이후 간단하게 submitList 메서드를 통해 리스트를 update할 수 있다.

 

 

 

ListAdapter


 

다음은 ListAdapter에 대해 알아보자.

 

ListAdapter 클래스는 내부에 AsyncListDiffer가 정의되어 있고 이를 사용하는 Adapter이다.

 

아래 코드를 보면 AsyncListDiffer를 내부적으로 사용하는 것을 알 수 있다.

 

public abstract class ListAdapter<T, VH extends RecyclerView.ViewHolder>
        extends RecyclerView.Adapter<VH> {
    final AsyncListDiffer<T> mDiffer;
    private final AsyncListDiffer.ListListener<T> mListener =
            new AsyncListDiffer.ListListener<T>() {
        @Override
        public void onCurrentListChanged(
                @NonNull List<T> previousList, @NonNull List<T> currentList) {
            ListAdapter.this.onCurrentListChanged(previousList, currentList);
        }
    };

    @SuppressWarnings("unused")
    protected ListAdapter(@NonNull DiffUtil.ItemCallback<T> diffCallback) {
        mDiffer = new AsyncListDiffer<>(new AdapterListUpdateCallback(this),
                new AsyncDifferConfig.Builder<>(diffCallback).build());
        mDiffer.addListListener(mListener);
    }

    @SuppressWarnings("unused")
    protected ListAdapter(@NonNull AsyncDifferConfig<T> config) {
        mDiffer = new AsyncListDiffer<>(new AdapterListUpdateCallback(this), config);
        mDiffer.addListListener(mListener);
    }

    // Submits a new list to be diffed, and displayed
    public void submitList(@Nullable List<T> list) {
        mDiffer.submitList(list);
    }

    // Set the new list to be displayed
    public void submitList(@Nullable List<T> list, @Nullable final Runnable commitCallback) {
        mDiffer.submitList(list, commitCallback);
    }

    protected T getItem(int position) {
        return mDiffer.getCurrentList().get(position);
    }

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

    // Get the current List
    @NonNull
    public List<T> getCurrentList() {
        return mDiffer.getCurrentList();
    }

    // Called when the current List is updated
    public void onCurrentListChanged(@NonNull List<T> previousList, @NonNull List<T> currentList) {
    }
}

 

 

이렇게 ListAdapter를 사용하게 되면 아래와 같이 boilerplate를 줄이고 Adapter를 구현할 수 있게 된다.

 

 

class SearchBookListAdapter : ListAdapter<BookDocument, SearchBookViewHolder>(diffUtil) {

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SearchBookViewHolder { ... }

    override fun onBindViewHolder(holder: SearchBookViewHolder, position: Int) { ... }

    companion object {
        val diffUtil = object : DiffUtil.ItemCallback<BookDocument>() {
            override fun areItemsTheSame(oldItem: BookDocument, newItem: BookDocument): Boolean {
                return oldItem.url == newItem.url
            }

            override fun areContentsTheSame(oldItem: BookDocument, newItem: BookDocument): Boolean {
                return oldItem == newItem
            }
        }
    }
}

 

 

 

정리


ListAdapter를 사용하면

  • 기존 아이템 List와 새로운 아이템 List를 비교해서 바뀐 아이템만 바꿔준다
  • 내부적으로 background thread에서 비교 연산을 수행한다
  • boilerplate code를 줄여 개발 공수를 줄일 수 있다