post-custom-banner

배경

기존의 리사이클러뷰를 사용할 때에는 리스트가 바뀔때마다 데이터를 업데이트 해야 했다.
예를들면 리스트의 아이템을 삭제하거나 추가할때마다 notifyDataSetChanged()를 통하여 리사이클러뷰에게 데이터 변화를 알려주고 리스트의 전체를 업데이트하고 바뀐 데이터들을 가져와 리사이클러뷰를 통해 보여주었다.

하지만 이런 과정에서 지연이 길어지고, 목록의 내용이 변경되는 동안의 비용이 많이 들었다.
데이터를 새로 가져오기 위해서 adapter는 새로운 item 인스턴스를 만들어주어야 하기 때문이다.

이를 해결하기 위해 DiffUtil이 나왔다.


DiffUtil

RecyclerView의 Support Library에 포함된 유틸리티 클래스이다.
이 클래스는 두 목록간의 차이점을 찾고 업데이트 되어야 할 목록을 반환시켜준다.
Eugiene W.Myeers's의 차이 알고리즘을 이용하여 업데이트 수를 계산한다.
DiffUtil 사용을 위해서는 바뀌기 전 후의 리스트를 알고있어야 한다.


작동 원리

  • getOldListSize(): 이전 목록의 개수를 반환합니다.

  • getNewListSize(): 새로운 목록의 개수를 반환합니다.

  • areItemsTheSame(int oldItemPosition, int newItemPosition): 두 객체가 같은 항목인지 여부를 결정합니다.

  • areContentsTheSame(int oldItemPosition, int newItemPosition): 두 항목의 데이터가 같은지 여부를 결정합니다. areItemsTheSame()이 true를 반환하는 경우에만 호출됩니다.

  • getChangePayload(int oldItemPosition, int newItemPosition): 만약 areItemTheSame()이 true를 반환하고 areContentsTheSame()이 false를 반환하면 이 메서드가 호출되어 변경 내용에 대한 데이터를 가져옵니다.


사용 방식

먼저 DiffUtil.Callback을 구현한 클래스를 만든다.

class PersonDiffCallback(
    private val oldList: List<Person>,
    private val newList: List<Person>
) : 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]
}

이 Callback을 RecyclerView의 리스트 업데이트 하는 함수에 추가한다.

class PersonDiffAdapter : RecyclerView.Adapter<PersonViewHolder>() {
    private val people = mutableListOf<Person>()
    
    ...
    
    fun replaceItems(newPeople: List<Person>) {
        val diffCallback = PersonDiffCallback(people, newPeople)
        val diffResult = DiffUtil.calculateDiff(diffCallback)
        
        people.clear()
        people.addAll(newPeople)
        
        diffResult.dispatchUpdatesTo(this)
    }
}
  • calculateDiff() : Diff알고리즘을 통해 변경된 아이템을 감지한다.
  • dispatchUpdatesTo() : 지정된 어댑터로 이벤트를 전달한다.

주의해야할 점

calculateDiff는 아이템의 개수가 많아지게 된다면 연산시간이 길어지기 때문에 백그라운드쓰레드에서 처리한다.

💡 DiffUtil에서 dispatchUpdatesTo()의 작동 원리.

diff계산에서 반환된 DiffResult 객체가 변경사항을 Adapteer에 전달하고 알림을 받는다. 이 알림은 notify-메서드로 변경사항에 대해 리스트의 아이템이 업데이트 된다.


AsyncListDiffer

DiffUtil을 더 단순하게 사용할 수 있게 해주는 클래스이다. 자체적으로 쓰레드에 대한 처리가 되어있기 때문에 동기화 처리를 할 필요가 없다.

AsyncListDiffer 객체를 선언해서 사용.

submitList를 통해 데이터를 교체할때 사용하며 Adapter에 새 목록을 전달한다.
어댑터 업데이트는 백그라운드 스레드에서 계산된다.

getCurrentList를 통해 현재 목록을 가져올수 있으며 List<>타입으로 반환되어 수정이 불가능합니다.

class PlaceRecyclerAdapter : RecyclerView.Adapter<PlaceViewHolder>() {
    private val diffUtil = AsyncListDiffer(this, PlaceDiffUtilCallback())

    fun replaceTo(newItems: List<Place>) = diffUtil.submitList(newItems)

    fun getItem(position: Int) = diffUtil.currentList[position]

    override fun getItemCount(): Int = diffUtil.currentList.size

    override fun onBindViewHolder(holder: PlaceViewHolder, position: Int) =
        holder.bind(getItem(position))

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = PlaceViewHolder(
        ItemPlaceBinding.inflate(LayoutInflater.from(parent.context), parent, false)
    )
}

ListAdapter

AsyncListDiffer를 더 쓰기 편하게 래핑한 클래스이다.
RecyclerView에서 Adapter를 상속할때 ListAdapter를 상속하면 된다.
초기화 할때 DiffUtil콜백 객체를 받도록 하면 AsyncListDiffer와 같이 currentList로 현재 데이터를 불러올수 있고 submitList로 데이터를 갱신할 수 있다.

외부에서 아이템리스트를 교체하는 replaceTo같이 구현해야하지만 이것조차 할 필요 없게만든다.

Adapter 에서 ListAdapter를 상속받고 파라미터에 diffUtil의 callback을 구현해서 넘겨준다.

class PlaceRecyclerAdapter : ListAdapter<Place, PlaceViewHolder>(PlaceDiffUtilCallback()) {

    override fun onBindViewHolder(holder: PlaceViewHolder, position: Int) =
        holder.bind(getItem(position))

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = PlaceViewHolder(
        ItemPlaceBinding.inflate(LayoutInflater.from(parent.context), parent, false)
    )
}

외부에서 submitList로 데이터를 넘겨준다.

val placeAdapter = PlaceRecyclerAdapter()
placeAdapter.submitList(newItems) // 아이템 업데이트
profile
러닝커브를 따라서 등반중입니다.
post-custom-banner

0개의 댓글