Recycler View

데이터를 리스트 형태로 화면에 표시하는 컨테이너 역할을 수행합니다.

어뎁터

  • 리사이클러뷰에 표시될 아이템 뷰를 생성하는 역할은 어댑터가 담당합니다. 사용자 데이터 리스트로부터 아이템 뷰를 만드는 것, 그것이 바로 어댑터가 하는 역할입니다.

https://recipes4dev.tistory.com/154

레이아웃 메니저

  • 레이아웃매니저는 리사이클러뷰가 아이템을 화면에 표시할 때, 아이템 뷰들이 리사이클러뷰 내부에서 배치되는 형태를 관리하는 요소입니다.

뷰 홀더

  • 어댑터에 의해 관리되는데, 필요에 따라(좀 더 정확히는, 레이아웃매니저의 아이템 뷰 재활용 정책에 따라) 어댑터에서 생성됩니다. 물론, 미리 생성된 뷰홀더 객체가 있는 경우에는 새로 생성하지 않고 이미 만들어져 있는 뷰홀더를 재활용하는데, 이 때는 단순히 데이터가 뷰홀더의 아이템 뷰에 바인딩(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를 통해서 살짝 다른값을 넣어주게 됩니다. 

 

왼쪽은 payload적용전 오른쪽은 payload적용 후 입니다. 확실히 깜빡거리는것이 없어진것을 알 수 있습니다.

 

Reference

728x90
  • 네이버 블러그 공유하기
  • 네이버 밴드에 공유하기
  • 페이스북 공유하기
  • 카카오스토리 공유하기