본문 바로가기
개발/Android

[Android] 리스트 업데이트를 위한 ListAdapter

by tempus 2022. 6. 7.
반응형

들어가기 전에, 왜 ListAdapter를 사용하는지 알아봅시다. 일단 RecyclerView에서 일반적으로 아이템을 갱신하기 위해 notfiyItem 메서드를 많이 사용합니다. 하지만 이는 불필요한 아이템들도 일일이 갱신하기 때문에 불필요한 비용이 들어간다는 단점이 있습니다. 여기서 우리는 변경해야 하는 아이템들만 변경해주기 위해서 ListAdapter를 사용합니다.

 

ListAdapter의 기원 DiffUtil

ListAdapter를 알아보기 전에 기원이 되는 Util 클래스인 DiffUtil에 대해 알아봅시다. 이름에서 알 수 있듯이 차이와 관련된 클래스인 것 같다는 생각이 드실 겁니다. 맞습니다. 이 클래스는 다음과 같은 역할을 합니다.

두 데이터 셋을 받아서 그 차이를 계산해주는 클래스

 

DiffUtil을 사용하기 위해서는 Diffutil.Callback()이라는 추상 클래스를 구현해주어야 합니다. 예제를 통해 한번 보면 다음과 같습니다.

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

    override fun getNewListSize() = newList.size

    override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int) =
        oldList[oldItemPosition].id == newList[newItemPosition].id

    override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int) =
        oldList[oldItemPosition] == newList[newItemPosition]
}
  • getOldListSize() : 이전 목록의 크기를 반환
  • getNewListSize() : 새로 들어온 목록의 크기를 반환
  • areItemsTheSame() : 두 아이템이 같은 객체인지 판단
  • areContentsTheSame() : 두 아이템이 같은 데이터를 갖고 있는지 여부를 반환, areItemsTheSame()true일 때 호출 됩니다.

 

이제 리스트 업데이트를 위해 기존 adpater에 다음과 같이 추가해주면 됩니다.

class UserAdapter : RecyclerView.Adapter<UserViewHolder>() {
    private val user = mutableListOf<User>()
    
    ...
    
    fun replaceItems(newUser: List<user>) {
        val diffCallback = UserDiffCallback(user, newUser)
        val diffResult = DiffUtil.calculateDiff(diffCallback)
        
        user.clear()
        user.addAll(newUser)
        
        diffResult.dispatchUpdatesTo(this)
    }
}
  1. calculateDiff()에서 diff 알고리즘을 통해 변경된 아이템을 감지합니다.
  2. dispatchUpdatesTo()에서 지정된 Adapter로 업데이트 이벤트를 전달합니다.

 

이때 DiffUtil은 아이템 개수가 많아지면 calculateDiff()diff 계산 시간이 길어질 수 있기 때문에 백그라운드 스레드에서 처리해주어야 합니다.

 

DiffUtil 클래스를 사용하기 위해 개발자는 직접 백그라운드 스레드에서 비교 처리를 수행하고 결과를 메인 스레드에서 처리하는 코드를 작성해야 합니다. 이런 작업을 줄이기 위해 AsyncListDiffer가 나왔고 이를 더 쉽게 사용하기 위해 이제 우리가 알아야 할 ListAdpater가 만들어졌습니다. (AsyncListDifferListAdpaer는 내부적으로 diff 계산을 백그라운드 스레드로 처리하기 때문에 우리는 스레드를 신경 쓰지 않아도 된다.)

 

여기서는 AsyncListDiffer를 건너뛰고 ListAdpater의 사용법을 알아볼 겁니다. 참고로 ListAdapter는 내부적으로 AsyncListDiffer를 사용하고 있습니다.

 

ListAdapter 사용법

class KeywordAdapter : ListAdapter<KeywordEntity, RecyclerView.ViewHolder>(DiffCallback()) {

    inner class ViewHolder(private val binding : KeywordItemBinding) : RecyclerView.ViewHolder(binding.root){
        fun bind(data : KeywordEntity) {
        		/**binding data**/
        }
    }
  
    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
        val binding = KeywordItemBinding.inflate(LayoutInflater.from(parent.context), parent, false)
        return ViewHolder(binding)
    }

    override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
        if(holder is ViewHolder) holder.bind(getItem(position))
    }

}

private class DiffCallback : DiffUtil.ItemCallback<KeywordEntity>(){
    override fun areItemsTheSame(oldItem: KeywordEntity, newItem: KeywordEntity): Boolean {
        return oldItem.id == newItem.id
    }

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

먼저 아이템을 비교할 때 호출할 DiffUtil.ItemCallback을 구현합니다. 그리고 난 후 해당 adapter에 ListAdapter<"모델", "사용할 ViewHolder">("구현한 DiffUtil.ItemCallback")을 상속받으면 됩니다. 이를 이제 실질적으로 업데이트해줄 때는 다음과 같이 해주면 됩니다.

 

Fragment나 Activity에서 사용할 때는 아래와 같이 사용하면 됩니다.

val keywordAdpater = KeywordAdapter()
keywordAdpater.submitList(newItems) // 아이템 업데이트

만약 LiveData를 사용하고 있다면 다음과 같이 활용할 수 있습니다. (아래는 Fragment 일부)

private val adapter = KeywordAdapter()
private val keywordViewModel by activityViewModels<KeywordViewModel>()


 override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)


        keywordViewModel.getAll().observe(viewLifecycleOwner) { keywords ->
            adapter.submitList(keywords)
        }

 }

 

결론

정리하면, ListAdapter를 사용하면 변경된 아이템만 UI를 업데이트할 수 있습니다. 또한 기존의 보일러 플레이트 코드도 엄청나게 줄여주며, 스레드 작업과 애니메이션 작업까지 알아서 다 해준다는 장점이 있습니다. 이를 LiveData와 함께 활용을 한다면 데이터를 관찰하여 변경 시에 업데이트를 해줄 수 있어 좋은 시너지를 내줄 수 있습니다.

 

참고 사이트

 

 

반응형

댓글


loading