[Kotlin Android] RecyclerView 어댑터의 데이터 빠르게 바꾸기 - ListAdapter와 DiffUtil 사용하기

이현우·2020년 11월 27일
5

Android 기능 구현

목록 보기
9/13
post-thumbnail

글을 읽기 전 참고 사항
ViewHolder에서 DataBinding과 AAC LiveData를 활용하였습니다. 구현하실 때에는 크게 상관은 없지만 혹시 읽는데 불편할 경우 해당 내용을 먼저 공부하고 오는 것을 추천드립니다.

RecyclerView ListAdapter

Adapter에서 notifyDataSetChanged()를 쓰면 힘든(?) 이유

RecyclerView에서 어댑터가 하는 일은

  • 미리 생성해둔 뷰홀더 객체
  • 사용자가 원하는 데이터 리스트를 주입하고
  • 데이터 리스트의 변경사항이 있을 때 이를 UI에 반영

하는 것이다.

그런데 Adapter에서 notifyDataSetChanged()를 사용하면

에잇 싯팔!
리스트 내의 데이터가 바뀌었으니까 리스트 다 바꿔
처신 잘하라고

그런데 만약에 한 번에 받은 Item들이 100~200개 이상이라면 통째로 업데이트할 때 지연 시간이 당연히 발생할 수밖에 없습니다. 그래서 RecyclerView에서는 지속적으로 Flickering Issue가 있어왔죠

(출처 - [Android/안드로이드]RecyclerView 화면깜빡임 현상 방지)

그래서 이런 비용을 줄이고자 Android 진영에서 내놓은 해결책은 DiffUtil입니다.

DiffUtil

RecyclerView.DiffUtil은

  • 현재 데이터 리스트와 교체될 데이터 리스트를 비교하고
  • 진짜 바뀌어야 할 데이터만 바꿔줌으로써
  • notify 뭐시기 보다 훨씬 빠른 시간 내에 데이터 교환을 할 수 있게 합니다.

그러나 아무리 DiffUtil이라도 메인 스레드에서 이런 비교 연산을 한다는 것 자체에 스레드 블락을 야기할 수 있기에 백그라운드 스레드에서 연산을 해야하고 이는 프로그래머로 하여금 비동기 처리까지 해야한다는 압박을 줬습니다.

그래서 AsyncListDiffer와 같은 비동기 처리를 지원해주는 DiffUtil 확장 클래스도 나왔지만, 이번 게시글에서 다룰 기능은 AsyncListDiffer보다 훨씬 사용하기 쉽기에 이에 대한 Practice를 공유하고자 합니다.

ListAdapter: 저와 같이 구현해보죠

ListAdapter는 DiffUtil을 활용하여 리스트를 업데이트할 수 있는 기능을 추가한 Adapter라고 생각하시면 될 것 같습니다. 기존 어댑터와 비교해서 추가로 DiffUtil 기능에 대한 콜백 기능 클래스만 구현하면 되므로 생산성, 효율성을 높일 수 있을 거라 생각합니다.

1. 아이템을 담을 레이아웃과 Data Class를 준비합니다

data class PopularGroupBookData(
	val txt_title: String
)

2. DiffUtilCallback 클래스를 정의합니다

위에서 DiffUtil은 데이터를 비교하여 필요한 부분만 교체한다는 설명만 했을 뿐 개발자가 이걸 어떻게 활용하지에 대해서는 안 말했습니다. 이 기능을 구현하면서 이야기해보도록 하죠.

DiffUtil을 사용하기 위해서 개발자는 DiffUtil.Callback이라는 기능을 구현해야 하는데요, 다음과 같은 사항들을 구현해야합니다.

  • areItemsTheSame : 이전 어댑터와 바뀌는 어댑터의 아이템이 동일한 지 확인을 합니다. 각 아이템의 고유 ID값(DB에서 key 같은 것을 받아온다면 그걸 활용)이 있을 것입니다. 이를 활용하여 비교를 해주시면 됩니다.

  • areContentsTheSame : 이전 어댑터와 바뀌는 어댑터의 아이템 내 내용을 비교합니다. areItemsTheSame 에서 true가 나올 경우 추가적으로 비교하기 위해서 사용하는 함수입니다.

만약 여기서 areItemsTheSame 를 잘못 정의하시면 모든 Item을 다 지우고 새 아이템을 Insert하는 일이 초래되어서 notifyDataSetChanged()와 별반 다를 바 없어질 수가 있습니다.

저는 Callback 클래스를 다음과 같이 object로 구현했습니다.

object MyDiffCallback : DiffUtil.ItemCallback<PopularGroupBookData>() {
    override fun areItemsTheSame(
        oldItem: PopularGroupBookData,
        newItem: PopularGroupBookData
    ): Boolean {
        return oldItem.txt_title == newItem.txt_title
    }

    override fun areContentsTheSame(
        oldItem: PopularGroupBookData,
        newItem: PopularGroupBookData
    ): Boolean {
        return oldItem == newItem
    }

}

3. ListAdapter를 구현하고 부착합니다

ListAdapter는 다음과 같은 형태를 지닙니다.

ListAdapter<데이터 클래스, 리사이클러뷰 뷰홀더>(개발자 정의 콜백)

ListAdapter는 데이터 클래스를 받고 있다는 게 특징인데요, 이는 사용자가 어댑터 내에서 데이터 리스트를 정의하지 않고 리스트 자체에서 데이터 리스트를 정의하기 때문입니다. 그래서 ListAdapter에서 구현해야 할 함수를 보면 getItemCount가 사라져 있는 것을 알 수 있습니다.

ListAdapter에서 사용할 수 있는 주요 Method
  • getItem(position: Int) : protected method라 클래스 내부에서 구현할 때 사용합니다. 어댑터 내 List Indexing을 할 때 활용됩니다
  • getCurrentList() : 어댑터가 가지고 있는 리스트를 가져올 때 사용합니다
  • submitList(MutableList<T> list) : 리스트 항목을 변경하고 싶을 때 사용합니다

다음과 같이 구현합니다.

class PopularGroupAdapter :
    ListAdapter<PopularGroupBookData, PopularGroupAdapter.PopularGroupViewHolder>(
        MyDiffCallback
    ) {
    inner class PopularGroupViewHolder(private val binding: ItemMainPopularGroupBinding) :
        RecyclerView.ViewHolder(binding.root) {
	// 그냥 View하고 데이터 연결하는 거 생각하면 됩니다
        fun bind(popularGroupBookData: PopularGroupBookData) {
            binding.popularBookData = popularGroupBookData
        }
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PopularGroupViewHolder {
        val layoutInflater = LayoutInflater.from(parent.context)
        val binding: ItemMainPopularGroupBinding =
            DataBindingUtil.inflate(layoutInflater, R.layout.item_main_popular_group, parent, false)
        return PopularGroupViewHolder(binding)
    }

    override fun onBindViewHolder(holder: PopularGroupViewHolder, position: Int) {
        holder.bind(getItem(position))
    }
}

이제 Fragment에서는

binding.recyclerView.adapter = popularGroupAdapter
// 뷰모델(데이터 저장하는 곳)에서 리스트가 변경될 때 반응하여 어댑터에 list를 넘겨줍니다
mainViewModel.popularGroupList.observe(viewLifecycleOwner, Observer { it ->
	it?.let { popularGroupAdapter.submitList(it) }
})

다음과 같이 구현하여 DiffUtil과 ListAdapter를 활용할 수 있습니다

profile
이현우의 개발 브이로그

0개의 댓글