본문 바로가기

Android(Kotlin) Study A부터 Z까지 등반

RecyclerView 이론

 

Recycler View란?

Recycler 재활용이 되는 ViewGroup이라고 생각하면 된다. 그림으로 보면 일반 리스트뷰와 크게 차이를 느낄 것이다.

 

  • 결론적으로 말하자면 기존 ListView보다 훨씬 더 효율을 지닌 리스트뷰가 탄생했다고 생각하면 된다.
  • 일반 리스트뷰 어댑터에 데이터를 담아놓는다면 리스트뷰는 한 방에 렌더링을 하면서 리소스를 많이 잡아먹게 된다. 만일 그게 1000000000개의 리스트가 필요하다면 어떻게 될까? 바로 터져버릴 것이다.(그전에 그냥 종료될 것이다)
  • 리사이클러뷰는 다르게 생성할 준비는 마친채로 데이터만 바인딩 되길 기다리는 여분의 리스트를 위 아래에 미리 띄운다고 생각하면 된다.

뭐가 재활용인가?

  • 위 아래로 여분의 리스트가 2개씩 있다고 친다면 스크롤을 확 올려 버렸을 때에는 생성, 삭제를 많이 하게 되어서 오히려 렉이 걸리지 않을까 오인하는 경우가 있다.
  • 하지만 생각과는 다르게 상단에 있던 여분의 뷰가 아래의 데이터로 내려오는 방식으로 재활용이 되고 있다. 그림을 참고하는게 이해가 가장 쉬울 것이다.
  • 결국엔 inflate를 최소화 하기 위함이다.

 💡 inflate : xml 기술된 View의 정의를 실제 VIew 객체로 만드는 것(메모리에 올리는 것)을 말함.

리싸이클러뷰는 3가지로 나눌 수 있다.

  • 뷰에 보여지는 영역(Adapter)
    • 뷰에 표시될 아이템 뷰를 생성하는 역할
    • 다양한 커스텀 리스트를 구현해낼 수 있다.
  • 아이템을 배치하는 영역(LayoutManager)
    • 리사이클러뷰가 아이템을 표시할 때 내부에 배치되는 형태를 관리
    • Linear, Grid, StaggeredGrid, etc..
  • 뷰에 보이지 않는 영역(ViewHolder)
    • 화면에 표시될 아이템 뷰를 저장하고 있는 곳
    • 어댑터에 의해 관리, 생성
    • 뷰 홀더의 아이템뷰에 데이터가 바인딩 되어 어댑터를 통해 화면에 보여짐

sub

  • ItemDecoration : 아이템 항목에서 서브뷰에 대한 처리 divider에 아이템을 추가할 수 있음.
  • ItemAnimation : 아이템 항목이 추가, 삭제되거나 정렬될 때 애니메이션 처리를 할 수 있다.

 💡 Adapter, ViewHolder와의 소통을 통해서 RecyclerView의 기능이 구현된다고 보면 됩니다.

 

ViewHolder란?

  • Holder패턴을 응용하여 만든 방법론
  • 위젯을 저장하기 위한 클래스
  • 보통은 Adapter class 내부에 class로 만들어서 사용한다.
  • 일반 리스트뷰도 해당 패턴을 사용하여 리스트뷰를 구성하면 성능은 같다.

커스텀 아이템을 구성할 때 ViewHolder를 상이하게 해서 다른 아이템을 보여줄 때 따로따로 구현해서 뷰 타입에 따라 나눠서 생성해준다.

새로운 아이템을 만들 때

여러 아이템을 조건에 따라 다르게 보여줄 때 예제코드

private inner class View1ViewHolder(itemView: View) :
        RecyclerView.ViewHolder(itemView) {
        var message: TextView = itemView.findViewById(R.id.textView)
        fun bind(position: Int) {
            val recyclerViewModel = list[position]
            message.text = recyclerViewModel.textData
        }
    }

private inner class View2ViewHolder(itemView: View) :
        RecyclerView.ViewHolder(itemView) {
        var message: TextView = itemView.findViewById(R.id.textView)
        fun bind(position: Int) {
            val recyclerViewModel = list[position]
            message.text = recyclerViewModel.textData
        }
    }
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
        if (viewType == VIEW_TYPE_ONE) {
            return View1ViewHolder(
                LayoutInflater.from(context).inflate(R.layout.item_view_1, parent, false)
            )
        }
        return View2ViewHolder(
            LayoutInflater.from(context).inflate(R.layout.item_view_2, parent, false)
        )
    }

    override fun getItemCount(): Int {
        return list.size
    }

    override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
        if (list[position].viewType === VIEW_TYPE_ONE) {
            (holder as View1ViewHolder).bind(position)
        } else {
            (holder as View2ViewHolder).bind(position)
        }
    }

    override fun getItemViewType(position: Int): Int {
        return list[position].viewType
    }
}

💡 이런 느낌으로 채팅창 같은 Custom List를 구현할 수 있습니다.

 

어댑터를 정의할 때는 세 가지 중요한 메서드를 Overring해서 구현합니당.

  • **onCreateViewHolder()**: RecyclerView는 ViewHolder를 새로 만들어야 할 때마다 이 메서드를 호출합니다. 이 메서드는 ViewHolder와 그에 연결된 View를 생성하고 초기화하지만 뷰의 콘텐츠를 채우지는 않습니다. ViewHolder가 아직 특정 데이터에 바인딩된 상태가 아니기 때문입니다.
  • **onBindViewHolder()**: RecyclerView는 ViewHolder를 데이터와 연결할 때 이 메서드를 호출합니다. 이 메서드는 적절한 데이터를 가져와서 그 데이터를 사용하여 뷰 홀더의 레이아웃을 채웁니다. 예를 들어 RecyclerView가 이름 목록을 표시하는 경우 메서드는 목록에서 적절한 이름을 찾아 뷰 홀더의 TextView 위젯을 채울 수 있습니다.
  • **getItemCount()**: RecyclerView는 데이터 세트 크기를 가져올 때 이 메서드를 호출합니다. 예를 들어 주소록 앱에서는 총 주소 개수가 여기에 해당할 수 있습니다. RecyclerView는 이 메서드를 사용하여, 항목을 추가로 표시할 수 없는 상황을 확인합니다.

 💡 호출되는 순서 getItemCount() -> onCreateViewHolder() -> onBindViewHolder()

 

notify

💡 notifyItemChanged notifyItemInserted notifyItemRemoved

 

요즘은 데이터의 변화에 대해서 DiffUtil를 사용하여 성능을 향상시킨다.

  • Eugene W. Myers’s의 차분 알고리즘을 이용함
  • 이전 데이터 상태와 현재 데이터간의 상태 차이를 찾고 업데이트 되어야 할 목록을 반환해줍니다.
  • RecyclerView 어댑터에 대한 업데이트를 알리는데 사용됩니다.
  • DiffUtil.Callback 추상 클래스를 콜백 클래스로 활용
  • 오버라이딩 메소드
    • getOldListSize(): 이전 목록의 개수를 반환합니다.
    • getNewListSize(): 새로운 목록의 개수를 반환합니다.
    • areItemsTheSame(int oldItemPosition, int newItemPosition): 두 객체가 같은 항목인지 여부를 결정합니다.
    • areContentsTheSame(int oldItemPosition, int newItemPosition): 두 항목의 데이터가 같은지 여부를 결정합니다. areItemsTheSame()이 true를 반환하는 경우에만 호출됩니다.
    • getChangePayload(int oldItemPosition, int newItemPosition): 만약 areItemTheSame()이 true를 반환하고 areContentsTheSame()이 false를 반환하면 이 메서드가 호출되어 변경 내용에 대한 페이로드를 가져옵니다.

notifyDataSetChanged는 왜 별로인가?

 💡 notifyDataSetChanged는 사실상 리스트를 싹 지우고 다시 처음부터 끝까지 객체를 하나하나 만들어 새로 렌더링하는 과정을 거치게 된다. 때문에 비용이 매우 크게 발생한다. 따라서 효율적으로 동적인 리사이클러뷰를 구성하는 방법이 필요했다.

 

DiffUtil.Callback 예제 코드

class DiffUtilCallback(private val oldList: List<Any>, private val newList: List<Any>) :
    DiffUtil.Callback() {
    override fun getOldListSize(): Int = oldList.size

    override fun getNewListSize(): Int = newList.size

    override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
        val oldItem = oldList[oldItemPosition]
        val newItem = newList[newItemPosition]

        return if (oldItem is Person && newItem is Person) {
            oldItem.id == newItem.id
        } else {
            false
        }
    }

    override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean =
        oldList[oldItemPosition] == newList[newItemPosition]
}
  • 그러고 난 뒤 리사이클러뷰의 어댑터에 아래와 같은 함수를 만들어두고, updateList() 를 통해 새로 들어온 데이터를 집어넣게 되면, DiffUtil.calculateDiff에 해당 데이터를 집어넣게 되고 인자로는 우리가 구현한 콜백 클래스 객체를 전달한다.
fun updateList(items: List<Person>?) {
    items?.let {
        val diffCallback = DiffUtilCallback(this.items, items)
        val diffResult = DiffUtil.calculateDiff(diffCallback)

        this.items.run {
            clear()
            addAll(items)
            diffResult.dispatchUpdatesTo(this@Adapter)
        }
    }
}
  • 이후 diffResult.dispatchUpdatesTo(어댑터)  를 호출하게 되면, 최소한의 업데이트 연산으로 리사이클러뷰를 갱신해줄 수 있는 것이다.

중요한점

  • 목록이 많으면 작업에 상당한 시간이 걸릴 수 있으므로 백그라운드 스레드에서 실행하고 DiffUtil.DiffResult를 가져와서 메인스레드(UI스레드)의 RecyclerView에 적용세요. 또한 구현 제약으로 목록의 최대 크기는 2²⁶개로 제한되어 있습니다.