• 목록 (128)
    • Android (62)
    • Back-End (2)
    • Java (3)
    • Kotlin (16)
    • CS (7)
    • 개발 서적 (12)
    • 문제 풀이 (26)

최근 글

티스토리

전체 방문자
오늘
어제
hELLO · Designed By 정상우.
MJ_94
Android

[Android] DiffUtil과 Payload를 활용한 RecyclerView 성능 최적화

[Android] DiffUtil과 Payload를 활용한 RecyclerView 성능 최적화
Android

[Android] DiffUtil과 Payload를 활용한 RecyclerView 성능 최적화

2024. 6. 18. 23:25

안드로이드에서 안 쓰는 앱을 더 찾기 힘들 정도로 자주 사용되는 RecyclerView는 매우 유용하면서도 늘 성능 최적화를 고려해야 하는 리스트뷰다.

성능 최적화 방법에는 여러 가지 접근 방식이 존재하겠지만, 가장 기본적이고 쉽게 RecyclerView의 성능을 최적화하는 방법이 있다.

결론부터 말하자면 리스트의 아이템에 변경사항이 있을 때 전체 리스트를 갱신하는 대신, DiffUtil의 payload를 활용하여 변경된 부분만 업데이트하는 방법으로 RecyclerView의 성능을 크게 향상할 수 있다.

만약 RecyclerView에 대량의 데이터가 노출되고 있을때, 리스트의 아이템이 빈번하게 변경될 때마다 전체 리스트를 갱신하는 방식은 비용이 매우 크며 성능 저하에 큰 영향을 미치게 된다.

DiffUtil은 RecyclerView의 데이터 세트가 변경될 때, 변경된 부분만을 찾아내어 효율적으로 업데이트하는 유틸리티 클래스다. 여기에 payload를 추가로 사용하면, 특정 부분만을 갱신하여 향상된 성능을 얻을 수 있다.

DiffUtil의 동작 Flow

DiffUtil의 동작 원리

DiffUtil은 두 데이터 세트의 차이를 계산하여 변경된 아이템을 찾아낸다. 이를 통해 불필요한 전체 갱신을 피하고, 애니메이션을 통해 부드러운 사용자 경험을 제공할 수 있다.

DiffUtil은 DiffUtil.Callback을 통해 동작하며, 주로 areItemsTheSame, areContentsTheSame, getChangePayload 세 가지 주요 메서드를 구현하게 된다.

각 메서드의 역할과 동작 방식은 아래와 같다.

1. areItemsTheSame

이 메서드는 두 아이템이 동일한 항목인지 확인한다. 대중적으로는 아이템의 고유 ID를 비교하여 두 아이템이 같은지 확인한다.

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

2. areContentsTheSame

이 메서드는 두 아이템의 콘텐츠가 동일한지 확인한다. areItemsTheSame이 true를 반환한 경우에만 호출되며, 모든 속성이 동일하면 true를 반환하고, 하나라도 다르면 false를 반환한다.

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

3. getChangePayload

이 메서드는 두 아이템 간의 특정 변화만을 반환한다. areItemsTheSame이 true이고 areContentsTheSame이 false인 경우에 호출된다. 이 메서드의 리턴타입은 Any이기 때문에 Bundle이나 다른 객체 형태로 유연하게 반환하여 RecyclerView.Adapter가 변경된 부분만 갱신할 수 있도록 한다.

override fun getChangePayload(oldItemPosition: Int, newItemPosition: Int): Any? {
    val oldItem = oldList[oldItemPosition]
    val newItem = newList[newItemPosition]

    val diffBundle = Bundle()
    
    if (oldItem.someProperty != newItem.someProperty) {
        diffBundle.putString("someProperty", newItem.someProperty)
    }

    return if (diffBundle.size() == 0) null else diffBundle
}

DiffUtil은 내부적으로 최적화된 LCS (Longest Common Subsequence) 알고리즘을 사용하여 두 리스트 간의 최소 변경 집합을 계산한다. 이를 통해 대규모 데이터 세트에서도 빠르고 효율적으로 변경 사항을 감지할 수 있다.

이러한 DiffUtil.Callback 클래스를 구현하여 두 데이터 세트 간의 비교 로직들을 작성한다.

class MyDiffCallback(
    private val oldList: List<MyItem>,
    private val newList: List<MyItem>
) : DiffUtil.Callback() {

    override fun getOldListSize(): Int {
        return oldList.size
    }

    override fun getNewListSize(): Int {
        return newList.size
    }

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

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

    override fun getChangePayload(oldItemPosition: Int, newItemPosition: Int): Any? {
        val oldItem = oldList[oldItemPosition]
        val newItem = newList[newItemPosition]

        val diffBundle = Bundle()
        
        if (oldItem.someProperty != newItem.someProperty) {
            diffBundle.putString("someProperty", newItem.someProperty)
        }

        return if (diffBundle.size() == 0) null else diffBundle
    }
}

Payload를 통한 효율적인 업데이트

예를 들어, 리스트 항목의 텍스트나 이미지와 같은 일부 속성만 변경된 경우 DiffUtil의 getChangePayload 메서드를 통해 변경된 부분만을 전달할 수 있고, 전체 항목을 다시 그릴 필요 없이 해당 속성만 갱신할 수 있다. 

아래 코드에서 onBindViewHolder 메서드는 payload가 존재하는지 검사하고, 변경된 부분만 업데이트할 수 있다.

override fun onBindViewHolder(holder: MyViewHolder, position: Int, payloads: MutableList<Any>) {
    if (payloads.isEmpty()) {
        super.onBindViewHolder(holder, position, payloads)
    } else {
        val diffBundle = payloads[0] as Bundle
        for (key in diffBundle.keySet()) {
            when (key) {
                "someProperty" -> {
                    holder.someTextView.text = diffBundle.getString(key)
                }
            }
        }
    }
}

 

RecyclerView에서 성능을 최적화하는 것은 사용자의 경험을 향상시키는 중요한 요소 중 하나이다. DiffUtil과 payload를 활용하면, 리스트의 변경사항을 효율적으로 처리할 수 있으므로 상황에 따라 전체 리스트를 갱신하는 대신, 변경된 부분만 업데이트하여 성능을 최적화하는 방법을 활용하면 좋을 것 같다.

저작자표시 비영리 변경금지 (새창열림)

'Android' 카테고리의 다른 글

[Android] ViewModel에서 LiveData와 StateFlow의 권장 사용 방법  (0) 2024.06.20
[Android] OS 10 이상에서 알림 갯수 제한 문제 해결  (0) 2024.06.19
[Android] RecyclerView 성능 최적화를 위한 ListAdapter 살펴보기  (0) 2022.12.22
[Android] Notification 커스텀하기  (0) 2022.12.21
[Android] Activity와 Fragment  (0) 2022.12.15
    'Android' 카테고리의 다른 글
    • [Android] ViewModel에서 LiveData와 StateFlow의 권장 사용 방법
    • [Android] OS 10 이상에서 알림 갯수 제한 문제 해결
    • [Android] RecyclerView 성능 최적화를 위한 ListAdapter 살펴보기
    • [Android] Notification 커스텀하기
    MJ_94
    MJ_94
    안드로이드, 개발 관련 기술 블로그

    티스토리툴바

    단축키

    내 블로그

    내 블로그 - 관리자 홈 전환
    Q
    Q
    새 글 쓰기
    W
    W

    블로그 게시글

    글 수정 (권한 있는 경우)
    E
    E
    댓글 영역으로 이동
    C
    C

    모든 영역

    이 페이지의 URL 복사
    S
    S
    맨 위로 이동
    T
    T
    티스토리 홈 이동
    H
    H
    단축키 안내
    Shift + /
    ⇧ + /

    * 단축키는 한글/영문 대소문자로 이용 가능하며, 티스토리 기본 도메인에서만 동작합니다.