본문 바로가기
Android/Android

[Android] RecyclerView DiffUtil

by Dani K 2021. 9. 16.

 

RecyclerView에 표현할 데이터를 업데이트하기 위해 주로 notifyDataSetChanged()를 호출한다.

 

notifyDataSetChanged()

 

리스트의 내용이 변경되어 notifyDataSetChanged()를 호출하면,

Adapter에게 RecyclerView의 데이터가 바뀌었으니 모든 항목을 통째로 업데이트를 하라는 신호를 보낸다.

이 방법은 모든 데이터를 다시 그리기 때문에 업데이트 과정이 지연되어 UX에 영향을 미칠 가능성이 매우 크다.

 

우리는 리스트를 매일 보고 사용한다.

변경이 된 데이터에 대해서만 Adapter 업데이트를 할 필요가 있는데 이를 위해 고안된 것이 DiffUtil 클래스다.

DiffUtil을 이용하여 데이터를 효율적으로 업데이트 할 수 있다.

 

DiffUtil

RecyclerView의 성능 향상을 위해 사용하는 DiffUtil은 old list와 new list를 비교하여 어떤 것이 다른지 알아낸다.

이 클래스는 Eugene W. Myers's difference algorithm을 이용하여 최소한의 업데이트 수를 계산한다.

DiffUtil이 어떤 것이 변경되었는지 알아내면 RecyclerView는 이 정보를 사용하여 변경, 추가, 제거 또는 이동된 항목만 업데이트 한다.

 

DiffUtil을 사용하는 방법은 다음과 같다. 먼저 DiffUtil.Callback을 구현한 클래스를 만들어야 한다.

 

class UserDiffCallback(
    private val oldList: List<User>,
    private val newList: List<User>
) : DiffUtil.Callback() {
    override fun getOldListSize() = oldList.size

    override fun getNewListSize() = newList.size

    override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int) =
        oldList[oldItemPosition].id == newList[newItemPosition].id

    override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int) =
        oldList[oldItemPosition] == newList[newItemPosition]
}

 

DiffUtil.Callback 구현이 완료되면 RecyclerView.Adapter의 리스트 업데이트 하는 함수에 추가한다. 

 

class UserDiffAdapter : RecyclerView.Adapter<UserViewHolder>() {
    private val user = mutableListOf<User>()
    
    ...
    
    fun replaceItems(newUser: List<User>) {
        val diffCallback = UserDiffCallback(user, newUser)
        val diffResult = DiffUtil.calculateDiff(diffCallback)
        
        user.clear()
        user.addAll(newUser)
        
        diffResult.dispatchUpdatesTo(this)
    }
}

 

calculateDiff()에서 Diff 알고리즘을 통해 변경된 아이템을 감지한다.

Diff 계산에서 반환된 DiffResult 객체가 dispatchUpdatesTo()를 통해 Adapter로 업데이트 이벤트를 전달한다.

 

목록이 크면 이 작업에 상당한 시간이 걸릴 수 있으므로 백그라운드 스레드에서 이 작업을 실행하고 DiffUtil.DiffResult를 가져온 다음 기본 스레드의 RecyclerView에 적용하는 것이 좋다.

 

AsyncListDiffer

DiffUtil은 아이템 수가 많으면 연산에 필요한 시간이 길어질 수 있기 때문에 백그라운드 스레드에서 처리하는 것이 좋다.

AsyncListDiffer은 내부적으로 diff 계산을 백그라운드 스레드로 처리한 뒤 리스트 업데이트까지 해 준다.

덕분에 우리는 스레드를 신경쓰지 않고 DiffUtil을 훨씬 편하게 사용할 수 있다.

 

먼저 DiffUtil.ItemCallback 클래스를 만든다.

 

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

    override fun areContentsTheSame(oldItem: User, newItem: User) =
        oldItem == newItem
}

 

그다음 RecyclerView Adapter에서 AsyncListDiffer를 생성해 사용하면 된다.

 

class UserAsyncDifferAdapter : RecyclerView.Adapter<UserViewHolder>() {
    private val asyncDiffer = AsyncListDiffer(this, UserDiffItemCallback())

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

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

    override fun getItemCount() = asyncDiffer.currentList.size

    fun replaceItems(newUser: List<User>) {
        asyncDiffer.submitList(newUser)
    }
}

 

currentList로 현재 아이템을 확인하고, submitList로 리스트 데이터를 변경한다. AsyncListDiffer에서 넘어오는 currentList는 READ ONLY 리스트로 변경이 불가능하기 때문에 currentList의 아이템의 변경은 submitList()를 통해서만 가능하다.

 

ListAdapter

AsyncListDiffer를 더 쓰기 편하도록 랩핑한 클래스가 바로 ListAdapter이다. 

ListAdapter는 사용자를 위해 목록을 추적하고 목록이 업데이트 될 때 어댑터에 알린다.

 

ListAdapter를 사용한 RecyclerView Adapter는 아래처럼 만들 수 있다.

 

class UserListAdapter : ListAdapter<User, UserViewHolder>(diffUtil) {

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

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

    fun replaceItems(items: List<User>) {
        submitList(items)
    }

    companion object {
        val diffUtil = object: DiffUtil.ItemCallback<User>() {
            override fun areContentsTheSame(oldItem: User, newItem: User) =
                oldItem == newItem

            override fun areItemsTheSame(oldItem: User, newItem: User) =
                oldItem.name == newItem.name
        }
    }
}
// Activity or Fragment ...
adapter.submitList(list)

 

•  getItem(position: Int) : ListAdapter 내부 List Indexing을 할 때 활용된다.

•  getCurrentList() : ListAdapter가 가지고 있는 리스트를 가져올 때 사용한다.

•  submitList(MutableList<T> list) : 리스트 항목을 변경하고 싶을 때 사용한다.

 

 

 DiffUtil과 ListAdapter를 활용한 예제

 

GitHub - keemdx/naver-search-android: Sample app using Naver Search API

Sample app using Naver Search API. Contribute to keemdx/naver-search-android development by creating an account on GitHub.

github.com

 

Reference

https://blog.kmshack.kr/RecyclerView-DiffUtil%EB%A1%9C-%EC%84%B1%EB%8A%A5-%ED%96%A5%EC%83%81%ED%95%98%EA%B8%B0/

https://developer.android.com/reference/android/support/v7/util/DiffUtil

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

 

 

댓글