[개념] 리사이클러뷰와 리스트 어댑터의 DiffUtil

쓰리원·2022년 4월 18일
0
post-thumbnail

1. Adapter의 역할.

Adapter가 맡은 역할은 크게 아래의 네 가지로 나눌 수 있습니다.

  1. xml의 마크업 코드를 객체로 inflate를 할 때, ViewHolder에 View 객체를 재사용.
  2. 화면에 RecyclerView를 통해 출력할 데이터(dataSet) 관리. ex) Data index 접근 및 data가 출력될 ViewHolder 변경 등이 가능하다.
  3. 경우에 따라 Click Event 관리.
  4. Adapter는 ListView와 RecyclerView의 position에 해당하는 데이터를 View에 Bind 하여 출력합니다.

2. DiffUtil 이란?

DiffUtil is a utility class that calculates the difference between two lists and outputs a list of update operations that converts the first list into the second one. [1]

DiffUtil은 Eugene W. Myers의 linear space refinement를 적용한 difference algorithm을 이용하여 두 리스트의 차이와 이전 리스트에서 새로운 리스트로 변환시키는 update 연산을 계산합니다. DiffUtil.calculateDiff(callback) method를 호출하면 DiffUtil.Result를 반환하며 Result클래스에는 RecyclerView를 결과에 맞게 update시켜주는 dispatchUpdatesTo(adapter) method가 존재합니다. 이를 이용하여 간단하게 두 리스트의 차이를 계산하여 RecyclerView에 반영시킬 수 있습니다.

DiffUtil.Callback

DiffUtil.calculateDiff(callback)에서 사용되는 callback 클래스입니다. List indexing과 item diffing을 담당하며 사용하기 위해서는 4개의 abstract method들을 구현해 주어야 합니다.

  1. int getOldListSize()
    이전 리스트의 크기를 반환 합니다.

  2. int getNewListSize()
    새로운 리스트의 크기를 반환 합니다.

  3. boolean areItemsTheSame(int oldItemPosition, int newItemPosition)
    두 아이템이 같은 아이템인지 반환합니다. 두 아이템을 구분하는 unique한 id가 존재할 경우 해당 id가 같은지 비교하면 됩니다.

  4. boolean areContentsTheSame(int oldItemPosition, int newItemPosition)
    areItemsTheSame가 true를 반환할 경우에만 호출되며 두 아이템이 같은 내용을 가지고 있는지 반환합니다.

DiffUtil.ItemCallback

DiffUtil.ItemCallback는 Callback클래스와 달리 item diffing만 담당합니다. 그러므로 ItemCallback을 이용하여 Callback은 list indexing만 담당하도록 관심사를 분리 시킬 수 있습니다.

  1. boolean areItemsTheSame(@NonNull T oldItem, @NonNull T newItem)
    두 아이템이 같은지 아이템인지 확인합니다. 여기서도 마찬가지로 uid가 존재한다면 비교해주도록 구현하면 됩니다.

  2. boolean areContentsTheSame(@NonNull T oldItem, @NonNull T newItem)
    areItemsTheSame이 true를 반환한 경우만 호출되며 두 아이템이 같은 내용을 가지고 있는지 확인합니다.

3. 리사이클러뷰와 리스트 어댑터의 DiffUtil의 사용시 차이점.

RecyclerView (setHasStableIds)

4. DiffUtil.ItemCallback 분석하기

abstract class BaseModel(
    open val id : Long,
    open val type : CellType
) {

    open fun isTheSame(item: BaseModel) : Boolean {
        return this.id == item.id && this.type == item.type
    }

    companion object {
        val DIFF_CALLBACK = object: DiffUtil.ItemCallback<BaseModel>() {
            override fun areItemsTheSame(oldItem: BaseModel, newItem: BaseModel): Boolean {
                return oldItem.isTheSame(newItem)
            }

            @SuppressLint("DiffUtilEquals")
            override fun areContentsTheSame(oldItem: BaseModel, newItem: BaseModel): Boolean {
                return oldItem == newItem
            }
        }
    }
}

위 코드에서 과거(oldItem)와 현재(newItem) 리스트의 차이를 비교 해서 areItemsTheSame과 areContentsTheSame를 override 해주는 것을 확인 할 수 있습니다.

아래의 그림으로 각 함수의 사용을 설명하겠습니다.

    open fun isTheSame(item: BaseModel) : Boolean {
        return this.id == item.id && this.type == item.type
    }
    
    override fun areItemsTheSame(oldItem: BaseModel, newItem: BaseModel): Boolean {
                return oldItem.isTheSame(newItem)
    }
            

areItemsTheSame은 위와 같이 id 값과 같은 고유값을 비교해서 같은 id인지 확인을 합니다. 이를 통해 이전 항목과 같은 것 인지 테스트를 하게 됩니다. 같으면 true를, 아니면 false를 리턴합니다.

여기서 areItemsTheSame에 false가 나오게되면 false가 나온 항목의 viewholder 를 다시 그려주게 됩니다. 그래서 깜빡거리는 현상이 나타나게 됩니다. true를 반환시에는 깜빡거리는 현상이 나타나지 않습니다.

false 의 경우 예시)

위의 예시는 전체가 깜빡이지만, areItemsTheSame은 항목 id 각각을 비교하여 flase를 반환한 경우 일치하지 않는 id만 viewholder부터 다시 그리고 안의 내용이 작성되게 됩니다. 그래서 true를 반환하는 id의 경우는 깜빡거리지 않습니다.

    @SuppressLint("DiffUtilEquals")
    override fun areContentsTheSame(oldItem: BaseModel, newItem: BaseModel): Boolean {
        return oldItem == newItem
    }

areContentsTheSame 은 areItemsTheSame true일 경우만 호출이 됩니다. 그래서 true가 반환이 된다면 viewholder는 다시 그리지 않기 때문에 깜빡거리는 것이 없게 data class 내부의 값 전체를 다 비교합니다. 비교 후 바뀐 Contents 값을 갱신 해줍니다.

true 의 경우 예시)

5. Team Project Yu Maket 적용 사례

테스트 mock data 2개의 List를 각각 준비합니다.

    override fun getAllMarketList(): List<TownMarketModel> {
        val mockList = listOf(
            TownMarketModel(
                id = 0, "쥬얼리 샵", true,
                LocationLatLngEntity(128.0, 36.0),
                "https://picsum.photos/200", 0.11f
            ),
            TownMarketModel(
                id = 1, "옷가게", true,
                LocationLatLngEntity(128.0, 36.0),
                "https://picsum.photos/200", 0.22f
            ),
            TownMarketModel(
                id = 2, "피자스쿨", true,
                LocationLatLngEntity(128.0, 36.0),
                "https://picsum.photos/200", 0.33f
            ),
            TownMarketModel(
                id = 3, "빅마트", false,
                LocationLatLngEntity(128.0, 36.0),
                "https://picsum.photos/200", 0.44f
            ),
            TownMarketModel(
                id = 4, "롯데리아", false,
                LocationLatLngEntity(128.0, 36.0),
                "https://picsum.photos/200", 0.10f
            )
        )

        return mockList
    }
override fun getAllMarketListtest(): List<TownMarketModel> {
        val mockList = listOf(
            TownMarketModel(
                id = 0, "쥬얼리 샵", true,
                LocationLatLngEntity(128.0, 36.0),
                "https://picsum.photos/200", 0.11f
            ),
            TownMarketModel(
                id = 1, "옷가게", true,
                LocationLatLngEntity(128.0, 36.0),
                "https://picsum.photos/200", 0.22f
            ),
            TownMarketModel(
                id = 2, "피자스쿨(test)", true,
                LocationLatLngEntity(128.0, 36.0),
                "https://picsum.photos/200", 0.33f
            ),
            TownMarketModel(
                id = 5, "빅마트(test)", false,
                LocationLatLngEntity(128.0, 36.0),
                "https://picsum.photos/200", 0.44f
            ),
            TownMarketModel(
                id = 4, "롯데리아(test)", false,
                LocationLatLngEntity(128.0, 36.0),
                "https://picsum.photos/200", 0.10f
            )
        )

        return mockList
    }

각 데이터들은 실험을 위해서 서로의 차이를 둬서(test)를 붙였습니다. 빅마트의 경우만 id 값이 3->5로 변경되었고 나머지는 id값의 변경이 없습니다. 옷가게와 쥬얼리 샵은 test를 붙이지 않아서 데이터의 변경이 일어나지 않게 하였습니다.

위와 같이 데이터 값을 정해놓고 areContentsTheSame 과 areItemsTheSame 을 어떻게 이해하고 사용하였는지 확인 해보겠습니다.

id값의 변환이 있기 때문에 뷰홀더를 다시 그려서 빅마트의 경우만 크게 깜빡거리는 것을 확인할 수 있습니다. areContentsTheSame에서 false를 반환했기 때문이죠. 나머지 경우 피자스쿨의 경우 살짝 반짝이는 경우를 알 수 있는데 제목이 test가 붙어서 변했기 때문에 뷰홀더를 다시 그리지는 않고 내부의 데이터값만 변환 되는 것을 알 수 있습니다.

6. reference

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

https://jaeryo2357.tistory.com/70
https://velog.io/@ilil1/%ED%8C%80-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-Yu-Market-%EC%B2%AB%EB%B2%88%EC%A7%B8-%EC%BD%94%EB%93%9C-%EB%A6%AC%EB%B7%B0-2%ED%8E%B8

profile
가장 아름다운 정답은 서로의 협업안에 있다.

0개의 댓글