티스토리 뷰

이전 포스팅에서 RecyclerView의 아이템을 LiveData로 관리하는 방법에 대해 알아보았다. 이 때 함께 사용하면 좋은 ListAdapter에 대해 소개하려고 한다.

 


 

RecyclerView를 사용하다보면 아이템을 변경할 일이 많고, 이럴 때 기존에는 notifyDataSetChanged()로 모든 아이템을 업데이트 하는 방법을 사용해 왔다. 하지만 이 방법은 아이템 개수가 많아질수록 비효율적일 수 밖에 없다. 물론 아래 notify- 메서드를 잘 이용하면 전체 데이터를 바꿀 필요가 없지만 position을 가지고 수동으로 관리해 주어야 하는 불편함이 있을 수 있고, 아이템의 정해진 순서가 없을 때는 이 기능을 사용할 수 없다.

 

RecyclerView 아이템의 변경사항을 notify하는 메서드들

 

이런 문제를 알고 구글에서는 DiffUtil이라는 매우 편리한 유틸리티 클래스를 만들었는데, 이 클래스는 두 리스트의 차이점을 찾아 업데이트 되어야 할 목록을 반환 해 줘서 RecyclerView 어댑터에 대한 업데이트를 알리는데 사용된다. 

 

 

DiffUtil 사용하기

DiffUtil을 사용하는 방법은 다음과 같다. 먼저 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)
    }
}

DiffUtil의 메서드의 역할은

 

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

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

 

추가로...

 

  1. DiffUtil은 dispatchUpdatesTo()에서 변경사항을 리스트에 적용할 때 추가 및 삭제 애니메이션을 제공한다.

  2. DiffUtil은 아이템 개수가 많을 경우 calculateDiff()의 diff 계산 시간이 길어질 수 있기 때문에 백그라운드 스레드에서 처리하는 것이 좋다.

  3. dispatchUpdatesTo()로 RecyclerView에 변경 사항을 적용하기 전에 원본 리스트를 새로운 리스트로 교체하는 작업을 해 줘야 한다.

 

여기서 3번에 대해 부가 설명을 하려고 한다. 이 부분은 이유를 알아보기 전에 일단 dispatchUpdatesTo()에서 하는 일을 알아야 한다. dispatchUpdatesTo()에서는 diff 계산에서 반환된 DiffResult 객체가 변경사항을 Adapter에 전달하고 Adapter가 변경 사항에 대해 알림을 받는다. 이 알림은 위에서 소개한 notify- 메서드로 변경 사항에 대해 리스트의 아이템이 업데이트 된다.

 

아래 코드는 DiffUtil에서 Adapter로 변경 사항을 notify 메서드로 알리는 부분이다.

DiffUtil에서 사용하는 notify- 메서드

 

 

아래 글은 DiffUtil 공식 문서에 나와 있는 dispatchUpdatesTo()에 대한 설명이다.

 

DiffUtil 공식 문서의 dispatchUpdatesTo() 설명

 

위 글을 읽어 보면, 리스트를 교체한 직후 업데이트 이벤트를 어댑터로 전송하고 그 이후에 RecyclerView가 리스트에 접근해야 하기 때문에 dispatchUpdatesTo()를 호출하기 전에 리스트를 변경해야 한다고 한다. 즉, dispatchUpdatesTo()에서는 데이터를 변경할 때 notify- 메서드로 즉시 어댑터로 업데이트 이벤트를 전달하기 때문에 그 전에 새로운 리스트로 교체를 해야 RecyclerView에 제대로 적용이 된다는 것이다.

 

또한 어댑터에서 사용하는 리스트는 MutableList로 만들어야 제대로 동작한다.

 

 

AsyncListDiffer 사용하기

DiffUtil 클래스를 사용하기 위해 개발자는 직접 백그라운드 스레드에서 비교 처리를 수행하고 결과를 메인 스레드에서 처리하는 코드를 작성해야 했다. 하지만 AsyncListDiffer 클래스는 이런 boiler plate 작업을 줄이기 위해 내부적으로 diff 계산을 백그라운드 스레드로 처리한 뒤 리스트 업데이트까지 해 준다. 덕분에 우리는 스레드를 신경쓰지 않고 DiffUtil을 훨씬 편하게 사용할 수 있게 되었다.

 

사용하는 방법 DiffUtil보다 훨씬 간단하다. 먼저 DiffUtil.ItemCallback을 구현한 클래스를 만들어야 한다.

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

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

 

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

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

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

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

    override fun getItemCount() = asyncDiffer.currentList.size

    fun replaceItems(newPeople: List<Person>) {
        asyncDiffer.submitList(newPeople)
    }
}

AsyncListDiffer에서

 

  1. submitList()를 호출하면 diffing 작업과 리스트 변경까지 진행하고,

  2. currentList로 현재 리스트의 아이템들을 확인할 수 있다.

AsyncListDiffer에서 넘어오는 currentList는 READ ONLY 리스트로 변경이 불가능하다. 그렇기 때문에 currentList의 아이템을 변경하기 위해서는 submitList()를 통해서만 가능하다.

 

 

ListAdapter 사용하기

AsyncListDiffer를 내부적으로 사용하고 있는 클래스가 바로 ListAdapter 클래스이다. 우리는 이제 그저 ListAdapter를 상속한 클래스를 만든 것 만으로도 AsyncListDiffer를 사용할 수 있게 되었다.

 

ListAdapter는 추상클래스로 아래 구현부 코드를 보면 우리가 AsyncListDiffer를 사용했을 때 코드처럼 RecyclerView의 Adapter를 상속하고, AsyncListDiffer를 만들어서 사용하고 있다. 그렇기 때문에 AsyncListDiffer의 submitList()currentList도 그대로 사용할 수 있다.

 

ListAdapter 구현부 코드 스니펫

 

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

class PersonListAdapter : ListAdapter<Person, PersonViewHolder>(diffUtil) {

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

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

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

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

            override fun areItemsTheSame(oldItem: Person, newItem: Person) =
                oldItem.name == newItem.name
        }
    }
}

 

위의 코드를 보면 기존 RecyclerView Adapter를 상속하는 방식과 달라진 점을 볼 수 있다.

 

  1. getItemCount() 오버라이딩 메서드가 없다.

  2. getItem(position) 메서드가 생겼다.

ListAdapter 내부에서 리스트 아이템을 관리하면서 getItemCount() 메서드가 재정의 되어 있고, 리스트의 아이템을 가져오는 getItem() 메서드가 생긴 것이다.

 

 

마무리

정리를 해 보자면, ListAdapter를 사용하면 변경 된 아이템만 UI를 업데이트 할 수 있고, 이를 위한 boiler plate 코드도 엄청나게 줄여주며, 스레드 작업과 애니메이션 작업까지 알아서 다 해준다.

약간의 불편한 점이라고는 리스트가 내부적으로 READ ONLY라서 변경을 위해서는 별도의 리스트를 두고 관리해야 한다는 정도지만, 이것도 RecyclerView의 데이터를 LiveData로 관리하는 경우에는 오히려 시너지를 내기 때문에 단점이라고 보기는 힘들 것 같다.

 

결론, ListAdapter를 사용하자!

 

 

 

 

[참고한 사이트]

 

댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/04   »
1 2 3 4 5 6
7 8 9 10 11 12 13
14 15 16 17 18 19 20
21 22 23 24 25 26 27
28 29 30
글 보관함