[Android, Kotlin] RecyclerView의 성능 개선, DiffUtil과 ListAdapter
Android

[Android, Kotlin] RecyclerView의 성능 개선, DiffUtil과 ListAdapter

RecyclerView의 Adapter는 RecyclerView에서 다음과 같은 역할을 한다.
데이터 리스트를 관리하여 포지션에 맞게 ViewHolder의 View와 연결하여 표시하는 중간자

기존 RecyclerView.Adapter를 사용할 경우 위 역할 중 데이터 리스트를 포지션에 맞게 표시하는 부분에서 비효율적인 방식이 나타난다.

기본적으로 새로운 데이터가 추가,삭제,변경 되었을 때 notifyDataSetChanged()를 통해 리스트 전체를 업데이트 한다.

private fun updateList(list: List<String>) {
   adapter.dataList = list
   adapter.notifyDataSetChanged() // 리스트 변경을 adapter에 알림
}


그러나 만약, 1000개의 데이터 중 단 한 개의 데이터만 바뀌었을 때에도 notifyDataSetChanged()를 사용하면 효율적일까?
아니다. 단 하나의 데이터를 바꾸기 위해 1000개의 데이터를 가진 리스트를 변경하는 작업을 수행하므로 효율적이지 못하다.

이 때 변경된 데이터의 position을 인자로 넘겨주어 해당 데이터만 변경하는 notifyItemChanged 메소드를 사용할 수 있다.
하지만 데이터가 변경 될 때마다 해당 position을 찾아 넘겨주며 하나하나 값을 변경하는 것... 여간 번거로운 일이다.

(1) DiffUtil


이러한 문제를 해결하기 위해 DiffUtil이라는 것이 존재한다.
DiffUtil은 oldItem, newItem의 두 데이터셋을 비교하여 값이 변경된 부분만을 RecyclerView에게 알려줄 수 있다.

object FlowerDiffUtil : DiffUtil.ItemCallback<Flower>() {

    override fun areItemsTheSame(oldItem: Flower, newItem: Flower): Boolean {
        return oldItem.id == newItem.id
    }

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


DiffUtil.ItemCallback을 통해 DiffUtil의 객체를 생성 할 수 있다.

areItemsTheSame 메소드에서는 두 아이템의 내부 데이터가 동일한지 확인한다. 만약 return 값이 false라면 데이터가 바뀐 것이므로 RecyclerView에 변경을 반영 할 수 있도록 한다. return 값이 true라면 areContentsTheSame 메소드를 호출하여 두 값이 동일한 아이템인지를 확인한다.

잘못 기재된 내용 수정 출처 : 댓글 - 라닉님

areContentsTheSame 메소드에서는 두 값이 동일한 아이템인지를 확인한다. return 값이 false라면 두 아이템의 내부 데이터가 동일하더라도 다른 아이템이므로 DiffUtil은 해당 데이터의 변경이 필요하다고 판단하고 RecyclerView에 반영 할 수 있도록 한다. return 값이 true라면 아이템과 데이터 모두 변경이 없는 것이므로 값의 변경을 반영하지 않는다.

DiffUtil의 경우, 리스트의 아이템이 많으면 하나하나 모두 비교 연산을 수행하므로 작업 시간이 길어질 수 있다.
그래서 안드로이드 공식문서에서는 DiffUtil의 비교 연산의 경우 백그라운드 스레드에서 처리하기를 권고하고 있다.

'목록이 크면 이 작업에 상당한 시간이 걸릴 수 있으므로 백그라운드 스레드에서 실행하라'

(2) AsyncListDiffer


이러한 DiffUtil을 백그라운드 스레드에서 수행할 수 있게 해주는 AsyncListDiffer가 존재한다.

public AsyncListDiffer(@NonNull RecyclerView.Adapter adapter,
        @NonNull DiffUtil.ItemCallback<T> diffCallback) {
    this(new AdapterListUpdateCallback(adapter),
        new AsyncDifferConfig.Builder<>(diffCallback).build());
}

Adapter와 DiffUtil을 인자로 받아 해당 DiffUtil을 백그라운드 스레드에서 수행하고 RecyclerView에 결과를 반영 할 수 있게 한다.

변경된 list를 asyncListDiffer의 submitList(list) 메소드를 통해 반영해 비교 할 수 있다.
RecyclerView에서 AsyncListDiffer를 적용하면 다음과 같다. (내용 설명에 필요한 메소드만 소스코드에 포함했다)

class FlowerAdapter(private val onClick: (Flower) -> Unit) 
	: RecyclerView.Adapter<FlowerAdapter.ViewHolder>() {

    private val asyncListDiffer: AsyncListDiffer<Flower> = AsyncListDiffer(this, FlowerDiffUtil)


    // 변경된 리스트 적용
    fun submitList(list: List<Flower>) {
        asyncListDiffer.submitList(list)
    }

    override fun onBindViewHolder(holder: ViewHolder, position: Int) {
        val flower = asyncListDiffer.currentList[position]
        holder.bind(flower)
    }
}

object FlowerDiffUtil : DiffUtil.ItemCallback<Flower>() {

    override fun areItemsTheSame(oldItem: Flower, newItem: Flower): Boolean {
        return oldItem.id == newItem.id
    }

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


onBindViewHolder 메소드에서 asyncListDiffer의 currentList[position]을 통해 리스트에서 현재 아이템을 꺼내 올 수 있다.
Adapter 클래스 내에 submitList 메소드를 만들어 asyncListDiffer에 변경된 리스트를 적용 할 수 있는 메소드를 외부에 노출시킨다.

이러한 AsynListDiffer를 사용하면 변경된 아이템에 대해서만 업데이트하고, 변경할 아이템의 position을 매번 찾을 필요가 없으므로
기존 notify~ 메소드를 이용한 것보다 효율적이다. 이 정도면 됐을까? 아니다.

RecyclerView에 AsyncListDiffer 객체를 생성하는 과정조차 불편하다고 느낀 Android는 한차례 더 자비를 베풀게 된다.

(3) ListAdapter


바로 ListAdapter라는 자비를 베풀어주신다.
ListAdapter는 AsyncListDiffer를 포함하는 클래스로, RecyclerView.Adapter 대신 ListAdapter를 사용함으로써 AsyncListDiffer 객체의 생성 없이도 백그라운드 스레드에서 DiffUtil의 비교 연산을 편하게 수행할 수 있게 한다.

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


ListAdapter의 경우 위와 같이 내부적으로 AsyncListDiffer 타입의 mDiffer를 가지고 있다.
해당 mDiffer를 통해 내부적으로 AsyncListDiffer의 동작을 수행한다.
제네릭타입 T에는 데이터 타입, VH에는 RecyclerView.ViewHolder를 상속받은 Adapter의 ViewHolder를 넣는다.

RecyclerView.Adapter 대신 ListAdapter를 사용하여 백그라운드 스레드에서 DiffUtil을 간단하게 이용한 예제를 보자.

class FlowerAdapter(private val onClick: (Flower) -> Unit) : ListAdapter<Flower, FlowerAdapter.ViewHolder>(FlowerDiffUtil) {

    class ViewHolder(itemView: View, onClick: (Flower) -> Unit) : RecyclerView.ViewHolder(itemView) {

        private val flowerTextView: TextView = itemView.findViewById(R.id.flower_tv)
        private var flower: Flower? = null

        init {
            itemView.setOnClickListener {
                flower?.let(onClick)
            }
        }

        fun bind(flower: Flower) {
            this.flower = flower
            flowerTextView.text = flower.name
        }
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
        val view = LayoutInflater.from(parent.context).inflate(R.layout.item_flower, parent, false)
        return ViewHolder(view, onClick)
    }

    override fun onBindViewHolder(holder: ViewHolder, position: Int) {
        holder.bind(getItem(position))
    }
}

object FlowerDiffUtil : DiffUtil.ItemCallback<Flower>() {

    override fun areItemsTheSame(oldItem: Flower, newItem: Flower): Boolean {
        return oldItem == newItem
    }

    override fun areContentsTheSame(oldItem: Flower, newItem: Flower): Boolean {
        return oldItem.id == newItem.id
    }
}


- ListAdapter는 위 코드와 같이 AsyncListDiffer 객체 없이도 백그라운드 스레드에서 DiffUtil 작업을 수행한다.
- 또한 내부적으로 아이템의 count 역시 처리하므로 getItemCount 메소드도 필수 구현이 아니다.
- currentList[position] 역시 사용 가능하며, 위 코드와 같이 getItem(position)을 통해서도 현재 아이템을 받아올 수 있다.
- ListAdapter 자체에서 submitList를 지원하므로 따로 submitList 메소드를 노출 시킬 필요 없이 바로 Adapter에서 submitList 함수를 사용할 수 있다.

flowerAdapter.submitList(newList)



이것으로 ListAdapter를 자랑하기 위한 빌드업을 마친다.
보일러 플레이트 코드를 줄이면서 성능의 최적화까지 가져올 수 있는 ListAdapter를 활용하는 습관을 기르도록 한다.


출처 : https://developer.android.com/reference/androidx/recyclerview/widget/DiffUtil

DiffUtil  |  Android Developers

developer.android.com


https://developer.android.com/reference/androidx/recyclerview/widget/AsyncListDiffer

AsyncListDiffer  |  Android Developers

developer.android.com


https://developer.android.com/reference/androidx/recyclerview/widget/ListAdapter

ListAdapter  |  Android Developers

developer.android.com


https://cliearl.github.io/posts/android/recyclerview-listadapter/

DiffUtil과 ListAdapter 이해하고 RecyclerView에 적용하기

이번 포스팅에서는 RecyclerView에 ListAdapter를 적용하는 법에 대해 알아보도록 하겠습니다. 들어가기 리사이클러뷰의 데이터가 변하면 리사이클러뷰 어댑터가 제공하는 notifyItem 메소드를 사용해서

cliearl.github.io