꾸준함이 진리다!!

어제보다 발전한 오늘이 되고픈 🧑🏻‍💻 의 블로그

Android/Kotlin

[Kotlin][Android] 안드로이드 ListAdapter와 DiffUtil , AsyncListDiffer를 활용한 RecyclerViewAdapter 구현하기

뎁요 2023. 2. 3. 13:20

안녕하세요 👋

오늘은 RecyclerView를 좀 더 효과적으로 사용하기 위한 ListAdapter, DiffUtil , AsyncListDiffer에 대해서

알아보도록 하겠습니다.

 

먼저 RecyclerView에 대한 포스팅은 아래에서 확인 부탁드립니다. 🙏

 

[Kotlin][Android] 안드로이드 RecyclerView(리싸이클러 뷰)의 사용방법

안녕하세요 👋 오늘은 RecyclerView에 대해서 공부한 내용을 포스팅해보려 합니다. RecyclerView(리싸이클러 뷰)란? RecyclerView는 ListView처럼 제한된 화면(Window)에 대용량 데이터 셋을 보여줄 때 사용합

devyo-111commit.tistory.com

 

DiffUtil이란?

RecyclerView를 사용할때 RecyclerView의 list를 해야 합니다.

업데이트를 위해서는 notifyDataSetChanged()(혹은 notifyItemRangeChanged())를 사용하는데 이 경우 list의 모든 data를 바꾸기 때문에 소수의 data만 변경되는데 해당 함수를 호출할 경우 성능적인 부분에서 문제가 발생할 수 있습니다.

물론 , notifyItemChanged()라는 함수가 존재하지만 해당 postion을 일일이 찾는 것은 매우 귀찮은 일입니다.

이러한 문제점 해결을 도와주기 위해 생겨난 라이브러리가 DiffUtil입니다.

 

DiffUtil을 사용하기 위해서는 우선 DiffUtil의 Callback class를 정의해주어야 합니다.

아래 각 추상 메소드들에 대해 설명드리도록 하겠습니다.

 

메소드 설명
getOldListSize() 변경되기전 List의 크기를 반환한다.
getNewListSize() 업데이트될 List의 크기를 반환한다.
areItemsTheSame(int oldItemPosition , int newItemPostion) 두Item이 같은 객체이면 true를 반환한다.(주로 item.id와 같은 객체의 유일값을 비교하는 경우가 많다.)
areContentsTheSame(int oldItemPosition , int newItemPosition) areItemsTheSame()가 true이면 호출되며 두 Item의 내용(Data)이 같은지 비교한다. 
getChangePayload(int oldItemPosition , int newItemPosition) areItemsTheSame()이 true이고 areContentsTheSame이 false 일때 해당 payload를 반환한다. (해당 메소드는 추상 메소드가 아니기에 굳이 재정의 할 필요가 없습니다.)

 

그렇다면 DiffUtil을 사용하는 방법은 어떻게 될까요? 🤔

 

class MainRecyclerViewAdapter: RecyclerView.Adapter<MainRecyclerViewAdapter.MainRecyclerViewHolder>(){
    
    ...  //생략
    
    private class DiffUtilCallBack(private val oldList : ArrayList<RecyclerItem> , private val newList : ArrayList<RecyclerItem>) : DiffUtil.Callback() {
        override fun getOldListSize() = oldList.size
        
        override fun getNewListSize() = newList.size
        
        override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
            val oldItem = oldList[oldItemPosition]
            val newItem = newList[newItemPosition]
            return  oldItem.title == newItem.title
        }

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

 

위 코드와 같이 RecyclerViewAdapter 내부에 DiffUtil의 Callback class를 구현하는 class를 따로 생성해 주었습니다.

 

class MainRecyclerViewAdapter: RecyclerView.Adapter<MainRecyclerViewAdapter.MainRecyclerViewHolder>(){
     //    ...  생략 
    fun updateItems(list : ArrayList<RecyclerItem>) { // item을 update하는 함수
        val diffUtil = DiffUtilCallBack(items , list)
        val result = DiffUtil.calculateDiff(diffUtil)
        items.run {
            clear()
            addAll(list)
        }
        result.dispatchUpdatesTo(this)
    }
    
    private class DiffUtilCallBack(private val oldList : ArrayList<RecyclerItem> , private val newList : ArrayList<RecyclerItem>) : DiffUtil.Callback() {
        override fun getOldListSize() = oldList.size
        
        override fun getNewListSize() = newList.size
        
        override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
            val oldItem = oldList[oldItemPosition]
            val newItem = newList[newItemPosition]
            return  oldItem.title == newItem.title
        }

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

 

다음으로는 adapter에 위와 같은 함수를 만들어 activity나 fragment에서 사용하면 됩니다.

 

 
 class MainActivity : AppCompatActivity() {
 	// ... 생략
      private fun observeData(){ // LivaData를 observe하는 함수
            with(viewModel){
                list.observe(this@MainActivity){
                    mainRecyclerViewAdapter.setItems(it)
                }
            }
        }
 }

위처럼 변경된 list를 전달해주기만 하면 DiffUtil에서 update를 위한 최적의 루트를 계산하여 자동으로 업데이트를 해주고

개발자로 하여금 notify를 해줄 필요 없이 dispatchUpdatesTo() 함수를 통해 손쉽게 list를 update 할 수 있습니다.

다만, 변경될 data가 너무 많아 DiffUtil에서 연산이 오래 걸릴 경우에는 Background Thread에서 동작을 해주고 해당 결과를 dispatch해주는 동작은 MainThread에서 동작하도록 구현하는 것이 좋습니다.

 

그렇다면 이번에는 AsyncListDiffer와 ListAdapter에 대해서 알아보도록 하겠습니다.

AsyncListDiffer란?

위에서 언급했듯 DiffUtil의 연산처리는 Background Thread에서 처리하는 것이 좋다고 했는데

그것을 도와주는 Helper class가 바로 AsyncListDiffer입니다.

주요 메소드로는 submitList()가 존재하며 AsyncListDiffer에게 변경될 List를 전달해 주는 역할을 합니다.

 

subList로 list가 전달되면 내부적으로는 Background Thread를 가져와 DiffUtil을 통한 연산을 진행하고

해당 결과에 따라 List를 업데이트시켜줍니다.

 

ListAdapter <T , VH>란?

기본적으로 ListAdapter는 RecyclerView.Adapter를 상속받습니다.

 내부적으로 DiffUtil과 AsyncListDiffer를 field로 가짐으로써 둘을 이용해 List update를 손쉽게 할 수 있게 도와줍니다.

 

그렇다면 이 둘은 어떻게 사용하는지 알아보겠습니다. 😎

private class DiffUtilCallBack : DiffUtil.ItemCallback<RecyclerItem>() {
        override fun areItemsTheSame(oldItem: RecyclerItem, newItem: RecyclerItem): Boolean {
            return oldItem.title == newItem.title
        }

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

 

먼저 ListAdapter를 사용할 때는 DiffUtil의 Callback class가 아닌 ItemCallback<T> class를 재정의 해주어야 합니다.

 

class MainRecyclerViewAdapter
    : ListAdapter<RecyclerItem , MainRecyclerViewAdapter.MainRecyclerViewHolder>(DiffUtilCallBack()){
    
    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MainRecyclerViewHolder {
        val binding = RecyclerItemBinding.inflate(LayoutInflater.from(parent.context) , parent , false)
        return MainRecyclerViewHolder(binding)
    }

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

    class MainRecyclerViewHolder(private val binding : RecyclerItemBinding) : RecyclerView.ViewHolder(binding.root){
        fun bind(item : RecyclerItem){
            with(binding){
                itemTitle.text = item.title
                itemImage.setImageResource(item.image)
            }
        }
    }

    private class DiffUtilCallBack : DiffUtil.ItemCallback<RecyclerItem>() {
        override fun areItemsTheSame(oldItem: RecyclerItem, newItem: RecyclerItem): Boolean {
            return oldItem.title == newItem.title
        }

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

 

위와 같이 RecyclerView.adapter를 상속받는 것이 아니라 ListAdapter <T , VH>를 상속받아 구현해주시면 되며

ListAdapter의 경우에는 RecyclerView.adapter와는 다르게 따로 List 만들어 관리 할 필요 없습니다.

(AsyncListDiffer에서 자동으로 관리됨 그렇기에 getItemCount는 재정의 할 필요가 없음)

 

list에 접근하기 위해서는 getItem(int position)을 활용하거나 getCurrentList()를 활용하면 되겠습니다.

다만, currentList를 가져올 경우에는 ReadOnlyList를 반환하기에

List를 통해 직접 변경하는 처리를 해야 할 경우(ItemTouchHelper를 통한 Drag and Drop 이벤트 등)

currentList를 통해 새로운 List객체를 생성하여 동작을 진행 후 변경된 list를 submitList()를 통해 update 해야 합니다.

 

 
 class MainActivity : AppCompatActivity() {
 	// ... 생략
      private fun observeData(){ // LivaData를 observe하는 함수
            with(viewModel){
                list.observe(this@MainActivity){
                    val newList = ArrayList(it)
                    mainRecyclerViewAdapter.submitList(newList)
                }
            }
        }
 }

마지막으로 위와 같이 adapter에 submitList()를 해주면 끝납니다.

(ListAdapter의 submitList()를 호출할 경우 AsyncListDiffer의 submitList()로 위임하여 AsyncListDiffer에서 처리함)

 

마지막으로 submitList()사용시 주의할 점은

submitList()는 내부적으로 같은 주소값을 가지고 있는 객체를 전달 받을 경우에는 객체를 비교하지 않습니다.

그렇게되면 update가 되지않는 현상이 발생하는데요

이를 해결하기 위해서는 위에서 보이듯 submitList()에 전달되는 객체를 새로 생성하여 전달해주어야 합니다.

 

이상으로 ListAdapter를 통한 RecyclerView 구현하는 방법을 마치도록 하겠습니다.

ListAdapter와 AsyncListDiffer , DiffUtil을 사용하면 보일러플레이트코드를 많이 개선할 수 있고

성능적으로도 많은 이득을 볼 수 있다고 생각합니다. 

 

어렵지 않은 방법이니 ListAdapter를 사용하는 습관을 많이 들여놔야겠습니다.

 

오늘도 즐코하세요 :)