리사이클러뷰를 구현하고 찾아보던 중 notifyDataSetChanged() 를 남발하면 불필요한 데이터 교체가 일어나 성능 저하가 일어난다는 사실을 알게 되었다
그래서 DiffUtil을 통해 성능을 개선할 수 있도록 기존 리사이클러뷰 어댑터를 수정해보았다
- DiffUtil은 두 목록의 차이를 계산하고 첫 번째 목록을 두 번째 목록으로 변환하는 업데이트 작업 목록을 출력하는 유틸리티 클래스입니다.
- RecyclerView 어댑터에 대한 업데이트를 계산하는 데 사용할 수 있습니다.
하지만 목록이 크면 이 작업에 상당한 시간이 걸릴 수 있으므로 AsyncListDiffer 를 이용하여 백그라운드 스레드에서 실행하는 것이 좋다
- 백그라운드 스레드에서 목록 간의 차이를 계산 하는 것을 포함하여 에 목록 데이터를 표시하기 위한 기본 클래스입니다 .
-> AsyncListDiffer를 포함하는 클래스로, DiffUtil을 활용해서 리스트 업데이트를 간단하게 사용할 수 있는 Adapter
- getCurrentList() : 현재 리스트 반환
- onCurrentListChanged() : 현재 리스트가 업데이트될 때 호출
- submitList(list) : 리스트 교체
기존 리사이클러뷰 어댑터와는 달리 getItemCount() 오버라이딩 메소드가 없고
getItem(position) 메소드가 생겼다
이제 헤더가 있는 리사이클러뷰에 리스트 어댑터를 구현해보자
데이터 항목 유형을 추상화하고 어댑터가 항목만 처리하도록 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
}
// 헤더 부분에 해당하는 뷰객체 가지는 뷰홀더
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")
}
}
}
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 -> {}
}
}
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)
}
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