안드로이드에서 안 쓰는 앱을 더 찾기 힘들 정도로 자주 사용되는 RecyclerView는 매우 유용하면서도 늘 성능 최적화를 고려해야 하는 리스트뷰다.
성능 최적화 방법에는 여러 가지 접근 방식이 존재하겠지만, 가장 기본적이고 쉽게 RecyclerView의 성능을 최적화하는 방법이 있다.
결론부터 말하자면 리스트의 아이템에 변경사항이 있을 때 전체 리스트를 갱신하는 대신, DiffUtil의 payload를 활용하여 변경된 부분만 업데이트하는 방법으로 RecyclerView의 성능을 크게 향상할 수 있다.
만약 RecyclerView에 대량의 데이터가 노출되고 있을때, 리스트의 아이템이 빈번하게 변경될 때마다 전체 리스트를 갱신하는 방식은 비용이 매우 크며 성능 저하에 큰 영향을 미치게 된다.
DiffUtil은 RecyclerView의 데이터 세트가 변경될 때, 변경된 부분만을 찾아내어 효율적으로 업데이트하는 유틸리티 클래스다. 여기에 payload를 추가로 사용하면, 특정 부분만을 갱신하여 향상된 성능을 얻을 수 있다.
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 |