[Android/Kotlin] Header가 있는 Recyclerview에 ListAdapter 적용하기 (DiffUtil, AsyncListDiffer)

SoyoungLee·2022년 5월 25일
0

안드로이드/코틀린

목록 보기
21/68
post-thumbnail

💌 [안드로이드/코틀린] Header가 있는 Recyclerview에 ListAdapter 적용하기 (DiffUtil, AsyncListDiffer)

리사이클러뷰를 구현하고 찾아보던 중 notifyDataSetChanged() 를 남발하면 불필요한 데이터 교체가 일어나 성능 저하가 일어난다는 사실을 알게 되었다
그래서 DiffUtil을 통해 성능을 개선할 수 있도록 기존 리사이클러뷰 어댑터를 수정해보았다

📌 DiffUtil 란?

  • DiffUtil은 두 목록의 차이를 계산하고 첫 번째 목록을 두 번째 목록으로 변환하는 업데이트 작업 목록을 출력하는 유틸리티 클래스입니다.
  • RecyclerView 어댑터에 대한 업데이트를 계산하는 데 사용할 수 있습니다.

하지만 목록이 크면 이 작업에 상당한 시간이 걸릴 수 있으므로 AsyncListDiffer 를 이용하여 백그라운드 스레드에서 실행하는 것이 좋다

📌 ListAdapter 란?

  • 백그라운드 스레드에서 목록 간의 차이를 계산 하는 것을 포함하여 에 목록 데이터를 표시하기 위한 기본 클래스입니다 .
    -> AsyncListDiffer를 포함하는 클래스로, DiffUtil을 활용해서 리스트 업데이트를 간단하게 사용할 수 있는 Adapter

메소드

  • getCurrentList() : 현재 리스트 반환
  • onCurrentListChanged() : 현재 리스트가 업데이트될 때 호출
  • submitList(list) : 리스트 교체

기존 리사이클러뷰 어댑터와는 달리 getItemCount() 오버라이딩 메소드가 없고
getItem(position) 메소드가 생겼다

이제 헤더가 있는 리사이클러뷰에 리스트 어댑터를 구현해보자

💜 Sealed Class 정의

데이터 항목 유형을 추상화하고 어댑터가 항목만 처리하도록 sealed class를 정의한다
서브 클래스들은 같은 파일 내에서 정의해줘야 한다

sealed class RecordItem {
    abstract val id: Long
    data class Item(val record: Record) : RecordItem() { // 아이템 항목
        override val id = medicalRecord.recordId
    }
    object Header : RecordItem() { // 헤더
        override val id = Long.MIN_VALUE
    }
    object Empty : RecordItem() { // 항목이 없을 때
        override val id = Long.MAX_VALUE
    }

}

헤더와 빈 데이터일 때 보여줄 뷰에는 실제 데이터가 없으므로 object 로 선언해준다 -> 하나의 인스턴스만 존재
DiffUtil은 항목의 id를 비교해서 변경되었는지를 확인하므로 abstract로 변수 선언해준다

💜 뷰타입을 구분 지어줄 상수

private const val HEADER = 0 // 헤더 뷰
private const val ITEM = 1 // 리사이클러 아이템 뷰
private const val EMPTY = 2 // 데이터가 없을 때 뜨는 뷰

💜 현재의 아이템 유형에 따라 사용할 뷰 항목 리턴

아이템이 없을 때만 헤더와 빈 화면 뷰타입을 리턴해준다

override fun getItemViewType(position: Int): Int {
        return when(getItem(position)){
            is RecordItem.Header -> HEADER
            is RecordItem.Item -> ITEM
            is RecordItem.Empty -> EMPTY
        }

💜 ViewHolder 추가

// 헤더 부분에 해당하는 뷰객체 가지는 뷰홀더
class HeaderViewHolder(val binding: RvItemHeaderBinding) :
    RecyclerView.ViewHolder(binding.root) {}
   
// 항목에 해당하는 뷰객체 가지는 뷰홀더
class ItemViewHolder(val binding: RvItemBinding) :
    RecyclerView.ViewHolder(binding.root) {
    	fun bind(item: RecordItem) {
    		with(binding) {
     			tvDate.text = item.date
            	tvName.text = item.name
        	}
    	}
    }
    
// 데이터가 없을 때 보여줄 부분에 해당하는 뷰객체 가지는 뷰홀더
class EmptyViewHolder(val binding: RvItemEmptyBinding) :
    RecyclerView.ViewHolder(binding.root) {}

💜 항목으로 사용할 뷰객체 생성

override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {

        return when (viewType) {
            HEADER ->
                HeaderViewHolder(
                    RvItemHeaderBinding.inflate(
                        LayoutInflater.from(parent.context),
                        parent,
                        false
                    )
                )
            ITEM -> 
                ItemViewHolder(
                    RvItemBinding.inflate(
                        LayoutInflater.from(parent.context),
                        parent,
                        false
                    )
                )
            EMPTY -> // EMPTY
                EmptyViewHolder(
                    RvItemEmptyBinding.inflate(
                        LayoutInflater.from(parent.context),
                        parent,
                        false
                    )
                )
            else -> {
                throw ClassCastException("Unknown viewType $viewType")
            }
        }
    }

💜 Holder 에 따른 바인딩 처리

override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {

        when (holder) {
            // HEADER
            is HeaderViewHolder -> {}
            // ITEM
            is ItemViewHolder -> {
            		val recordItem = getItem(position) as RecordItem.Item
                 	holder.bind(recordItem.item)
        }
        	is EmptyViewHolder -> {}
    }
}

💜 DiffUtilCallback 클래스 정의

object MyDiffCallback : DiffUtil.ItemCallback<RecordItem>() {
    override fun areItemsTheSame(
        oldItem: RecordItem,
        newItem: RecordItem
    ): Boolean { // 아이템이 동일한지
        return oldItem.id == newItem.id
    }

    override fun areContentsTheSame(
        oldItem: RecordItem,
        newItem: RecordItem
    ): Boolean { // 아이템이 동일하면 아이템 내용 비교
        return oldItem == newItem
    }
}

💜 목록을 가져와서 헤더를 추가한 후 리스트를 리턴

    fun addHeaderAndSubmitList(list: MutableList<Record>?) {
            val items = when(list.isNullOrEmpty()) {
                true -> listOf(RecordItem.Header) + listOf(RecordItem.Empty)
                false -> listOf(RecordItem.Header) + list.map { RecordItem.Item(it) }
            }
        submitList(items)
    }

💜 메인에서 submitList() 대신 호출

adapter.addHeaderAndSubmitList(newList.toMutableList())

참고 : https://developer.android.com/reference/kotlin/androidx/recyclerview/widget/ListAdapter
https://developer.android.com/reference/androidx/recyclerview/widget/AsyncListDiffer
https://developer.android.com/reference/androidx/recyclerview/widget/DiffUtil
https://hungseong.tistory.com/24
https://zion830.tistory.com/86
https://velog.io/@24hyunji/AndroidKotlin-RecyclerView%EC%97%90%EC%84%9C-ListAdapter-DiffUtil-%EC%82%AC%EC%9A%A9%ED%95%B4%EB%B3%B4%EA%B8%B0
https://yoojh9.tistory.com/entry/07-5-Headers-in-RecyclerView

profile
Android Developer..+ iOS 슬쩍 🌱 ✏️끄적끄적,,개인 기록용 👩🏻‍💻

0개의 댓글