Recycler View
데이터를 리스트 형태로 화면에 표시하는 컨테이너 역할을 수행합니다.
어뎁터
- 리사이클러뷰에 표시될 아이템 뷰를 생성하는 역할은 어댑터가 담당합니다. 사용자 데이터 리스트로부터 아이템 뷰를 만드는 것, 그것이 바로 어댑터가 하는 역할입니다.
레이아웃 메니저
- 레이아웃매니저는 리사이클러뷰가 아이템을 화면에 표시할 때, 아이템 뷰들이 리사이클러뷰 내부에서 배치되는 형태를 관리하는 요소입니다.
뷰 홀더
- 어댑터에 의해 관리되는데, 필요에 따라(좀 더 정확히는, 레이아웃매니저의 아이템 뷰 재활용 정책에 따라) 어댑터에서 생성됩니다. 물론, 미리 생성된 뷰홀더 객체가 있는 경우에는 새로 생성하지 않고 이미 만들어져 있는 뷰홀더를 재활용하는데, 이 때는 단순히 데이터가 뷰홀더의 아이템 뷰에 바인딩(Binding)됩니다.
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.
- 서로 다른 리스트를 비교해서 하나의 리스트로 만드는 역할을 합니다.
알고리즘
- Eugene W. Myers difference algorithm - This calculates the difference between both sets of elements.
- 시간 복잡도는 O(N^2)
Diff Util 사용시 문제점 - Unique 한 값을 사용하지 않을때
Adapter 에 제공할 DataClass
data class ItemDays(
val date: String,
val onSchedule: Boolean = false,
val isClicked: Boolean = false,
val isVisible: Boolean = false,
)
DiffUtil
private object ItemDiffUtil : DiffUtil.ItemCallback<CalendarDate>() {
override fun areItemsTheSame(
oldItem: CalendarDate,
newItem: CalendarDate,
) = oldItem.hashCode() == newItem.hashCode()
override fun areContentsTheSame(
oldItem: CalendarDate,
newItem: CalendarDate,
) = oldItem == newItem
}
- 위 DiffUtil 에서 문제점을 찾아본다면?
1. 깜빡임 문제
- 위 코드를 사용하면 현재 클릭시처럼 깜빡거리는 현상을 확인할 수 있습니다.
- 그 이유는?
DiffUtil 의 내부 코드
- DiffUtil 메서드 호출 순서
- calculateDiff - 새로운 List를 만들어줌
- diffPartial - 알고리즘을 통해서 두개의 서로다른 리스트의 다른 부분만 골라 하나의 리스트로 업데이트합니다.
- areItemsTheSame 이 먼저 호출되면서 아이템이 같은지 먼저 체크하게 됩니다. 만약 동일하다면 areContentsTheSame 으로 넘어가지만 여기서 값이 false이면 새로 화면을 그리게 됩니다.
- diffPartial 함수에서 areItemsTheSame 결과 정보를 토대로 areContentsTheSame 함수를 호출한 뒤에 DiffResult 이 실행되고 dispatchUpdatesTo 에서 리스트 갱신이 일어납니다.
- DiffResult 객체 내부 요약
return new DiffResult(cb, snakes, forward, backward, detectMoves);
- calculateDiff 를 통해서 결과값으로 나온 DiffResult 객체
- dispatchUpdatesTo 를 통해서 adapter에서 새로운 리스트로 업데이트를 하도록 사용됨
- dispatchUpdateTo 메서드에서 batcchingCallback의 dispatchLastEvent가 호출되면서 아래의 스위치 문에 따라 서로다른 메서드들이 실행됨.
public void dispatchLastEvent() {
if (mLastEventType == TYPE_NONE) {
return;
}
switch (mLastEventType) {
case TYPE_ADD:
mWrapped.onInserted(mLastEventPosition, mLastEventCount);
break;
case TYPE_REMOVE:
mWrapped.onRemoved(mLastEventPosition, mLastEventCount);
break;
case TYPE_CHANGE:
mWrapped.onChanged(mLastEventPosition, mLastEventCount, mLastEventPayload);
break;
}
mLastEventPayload = null;
mLastEventType = TYPE_NONE;
}
dispatchUpdatesTo에서는 다음 4가지 타입에 따라 화면갱신을 하는데 TYPE_NONE 은 areItemsTheSame 의 정보가 다른 경우에 호출이 됩니다.
- 즉 클릭한 부분의 정보와 클릭이 사라진 곳의 정보가 == 로 비교되면 아예 다르기 때문에 새롭게 부분들을 그려주는것입니다.
- 그래서 areItemsTheSame 을 비교하는 메서드에서는 객체마다의 고유의 id값을 비교하는것이 객체의 HashCode를 만들어 비교하는것보다 성능면에서 좋습니다.
2. DiffUtil 에서 DataClass의 값중 하나로 비교할때
- 그렇다면 비교할 값중에 date로 비교하는건 어떨까요? date도 충분히 Unique한 값이라고 생각했습니다.
- 단, 캘린더의 비어있는 칸의 date 는 “” 로 (빈 String값)으로 처리하였습니다. (다수의 비어있는 String값을 가진 item들 존재)
data class ItemDays(
val date: String,
val onSchedule: Boolean = false,
val isClicked: Boolean = false,
val isVisible: Boolean = false,
)
private object ItemDiffUtil : DiffUtil.ItemCallback<CalendarDate>() {
override fun areItemsTheSame(
oldItem: CalendarDate,
newItem: CalendarDate,
): Boolean {
val old = oldItem as CalendarDate.ItemDays
val new = newItem as CalendarDate.ItemDays
return old.date == new.date
}
override fun areContentsTheSame(
oldItem: CalendarDate,
newItem: CalendarDate,
) = oldItem == newItem
}
8월달 달력에서 왼쪽 오른쪽 옮길시 문제가 발생합니다.
- 왜 그럴까요?
- RecyclerView Adapter 내부 코드를 보자면 LinearLayoutManager 를 상속받고 있는데, 이 LinearLayoutManager가 내부적으로 ItemAnimator 를 사용합니다, 그리고 이 ItemAnimator가 animation 을 위해서 DiffUtil 을 사용하여 애니메이션을 동작시킵니다.
긴 설명을 읽어 보자면, 기존에 있던 리스트와 새로 들어온 리스트를 비교해서 이미 존재하는 애들은 재활용 하여 사용하겠다는 말입니다. 그리고 DiffUtil에서 Unique하지 않은 값으로 비교하는 부분때문에 여기에서 에러가 나는것을 알 수 있습니다.
- 구체적인 에러
결론 - 위 1번 2번 문제점을 통해서
- RecyclerView 에 제공되는 DataClass는 DiffUtil에 제공할 정말 Unique한 값을 가지고 있어야 하고, 이를 비교하는것이 성능에 좋습니다.
data class ItemDays(
val id: UUID,
val date: String,
val onSchedule: Boolean = false,
val isClicked: Boolean = false,
val isVisible: Boolean = false,
)
RecyclerView 성능 향상을 위한 방법
1. 각 어뎁터 마다 ListAdapter Background Thread 사용
public abstract class ListAdapter<T, VH extends RecyclerView.ViewHolder>
extends RecyclerView.Adapter<VH> {
final AsyncListDiffer<T> mDiffer;
protected ListAdapter(@NonNull DiffUtil.ItemCallback<T> diffCallback) {
mDiffer = new AsyncListDiffer<>(new AdapterListUpdateCallback(this),
new AsyncDifferConfig.Builder<>(diffCallback).build());
mDiffer.addListListener(mListener);
}
protected ListAdapter(@NonNull AsyncDifferConfig<T> config) {
mDiffer = new AsyncListDiffer<>(new AdapterListUpdateCallback(this), config);
mDiffer.addListListener(mListener);
}
// ...
}
- 대부분 ListAdapter를 만들때 DiffUtil.ItemCallBack<T>를 사용하는데 이를 AsyncDifferConfig방식으로 사용할 수도 있습니다.
- 내부적으로 두개의 생성자 모두 Background Thread에서 실행됩니다. AsyncListDifferConfig 를 설정해 주지 않는다면 Executors.*newFixedThreadPool*(2) 를 통해서 모든 ListAdapter들이 공유하는 쓰레드풀이 생성됩니다. (If not provided, defaults to two thread pool executor, shared by all ListAdapterConfigs.) 즉, 더 많은 연산을 해야 할때는 각각 ThreadPool을 새롭게 생성해주는것이 좋다고 가정할 수 있습니다.
이용방법
class MainAdapter(val action: (items: MutableList<Item>, changed: Item, checked: Boolean) -> Unit) :
ListAdapter<Item, MainAdapter.ItemViewHolder>(
AsyncDifferConfig.Builder<Item>(DiffCallback()).build())
위 화면처럼 여러개의 아이템을 지우는등 많은 연산을 할때 더욱 효과적으로 일처리가 가능해 집니다.
2. Payloads 이용하기
- 만약 하나의 itemView만 업데이트 하고 싶다면 payload를 이용하는것도 성능향상을 위한 방법입니다.
- 전체 리스트가 아니라 하나만 바꾸기 때문이죠.
1. DiffUtil 에서 payload override하기
override fun getChangePayload(
oldItem: CalendarDate,
newItem: CalendarDate,
): Any? {
if (oldItem is CalendarDate.ItemDays
&& newItem is CalendarDate.ItemDays
) {
if (oldItem.id == newItem.id) {
return if (oldItem.isClicked == newItem.isClicked) {
super.getChangePayload(oldItem, newItem)
} else {
val diff = Bundle()
diff.putBoolean(DONE, newItem.isClicked)
diff
}
}
}
return super.getChangePayload(oldItem, newItem)
}
- 위처럼 사용할때 Bundle에 달라진 값만 집어 넣어주면 Bundle 이 Adapter내에 payLoads에 들어가게 됩니다.
- isClicked 값이 다를 경우에만 집어 넣도록 설정
2. ViewHolder에서 Update함수 만들어주기
fun update(bundle: Bundle) {
if (bundle.containsKey(DONE)) {
val checked = bundle.getBoolean(DONE)
binding.isClicked = checked
}
}
뷰 홀더에서 업데이트를 해주게 되면 다른 부분은 다 동일하지만 업데이트 되는 부분만 바꿀 수 있습니다.
3. OnBindViewHolder Override
override fun onBindViewHolder(
holder: RecyclerView.ViewHolder,
position: Int,
) {
onBindViewHolder(holder, position, mutableListOf())
}
override fun onBindViewHolder(
holder: RecyclerView.ViewHolder,
position: Int,
payloads: MutableList<Any>,
) {
val item = getItem(position)
when (holder) {
is CalendarDayViewHolder -> {
if (payloads.isEmpty() || payloads[0] !is Bundle) {
holder.bind(item as CalendarDate.ItemDays)
} else {
val bundle = payloads[0] as Bundle
holder.update(bundle)
}
}
is CalendarDaysHeaderViewHolder -> holder.bind(item as CalendarDate.ItemHeader)
}
}
기존에 사용하던 onBindViewHolder 메서드와 새로운 palyloads를 가진 onBindViewHolder를 사용해 주는데 새로운 메서드에서 holder를 어떻게 bind할지 정의해 줍니다. 물론 palyload의 값이 있는 친구만 update를 통해서 살짝 다른값을 넣어주게 됩니다.
Reference
- https://recipes4dev.tistory.com/154
- https://thdev.tech/kotlin/2020/09/22/kotlin_effective_03/
- https://www.raywenderlich.com/21954410-speed-up-your-android-recyclerview-using-diffutil#toc-anchor-013
- https://blog.undabot.com/recyclerview-time-to-animate-with-payloads-and-diffutil-4278beb8d4dd
- https://readystory.tistory.com/216
728x90
최근댓글