ListAdapter란?

ListAdapter는 RecyclerView.Adapter의 확장된 형태로, RecyclerView의 데이터를 리스트 형태로 관리하며, 효율적인 데이터 리스트 업데이트를 위해 DiffUtil을 내부적으로 사용한다. 기존의 RecyclerViewAdapter와 ListAdapter의 주요 차이점은 데이터의 변경을 관리하는 방식에 있다.

 

조금 더 보태자면 기존의 RecyclerViewAdapter는 정적인, 즉 고정된 데이터를 보여주기에 최적화되어 있고 ListAdapter는 데이터가 동적으로 추가되고 삭제되는 일들이 많을 때 효율적으로 동작하기 위해 최적화되어있다고 생각하면 된다. 

RecyclerViewAdapter와의 차이점

RecyclerViewAdapter

  • 데이터 변경을 감지하고 UI를 업데이트하기 위해서는 개발자가 명시적으로 notifyDataSetChanged(), notifyItemInserted()notifyItemRemoved() 등의 메서드를 호출해야 한다.
  • 데이터가 변경될 때마다 전체 리스트를 업데이트하거나, 특정 위치의 데이터가 변경되었다는 것을 직접 알려줘야 한다.
  • 능상의 이유로, 대량의 데이터가 변경될 때 비효율적일 수 있다. ( 모든 아이템을 전부 다시 그리기 때문)

ListAdapter

  • 내부적 으로 AsyncListDiffer를 사용하여 데이터의 변경사항을 비동기적으로 계산하고, 필요한 최소한의 업데이트를 RecyclerView에 알려준다. (DiffUtil 메소드를 통해서 변경된 부분만 뷰를 그림)
  • 개발자는 단순히 새로운 리스트를 submitList() 메서드를 통해 전달하면, ListAdapter가 나머지를 처리합니다.
  • 데이터의 변경 사항이 자동으로 계산되어 효율적인 업데이트가 가능합니다.

DiffUtil이란?

DiffUtil은 두 리스트 간의 차이점을 계산하는 유틸리티 클래스로, 어떤 항목이 추가, 제거, 변경되었는지를 찾아낸다. 이를 위해 DiffUtil.Callback을 상속받은 클래스를 만들어서 두 리스트의 항목이 같은지(areItemsTheSame()), 내용이 같은지(areContentsTheSame())를 비교하는 로직을 구현해야 한다.


DiffUtil은 이러한 정보를 바탕으로 최소한의 업데이트로 RecyclerView를 갱신할 수 있는 'DiffResult'를 생성합니다. 이 결과는 RecyclerView의 어댑터에 적용되어, 효율적으로 UI를 업데이트할 수 있게 도와준다.

ListAdapter를 사용하면 이 모든 과정을 더 쉽게 처리할 수 있게 되어, 데이터가 변경될 때마다 RecyclerView를 더 효율적으로 업데이트할 수 있습니다.

 

ListAdapter 사용법

class ContactAdapter(var viewType: Int) :
    ListAdapter<ContactData, RecyclerView.ViewHolder>(diffUtil) {
    companion object {
        private val diffUtil = object : DiffUtil.ItemCallback<ContactData>() {
            override fun areItemsTheSame(oldItem: ContactData, newItem: ContactData): Boolean {
                // 연락처가 같은지 확인
                return oldItem == newItem
            }

            override fun areContentsTheSame(oldItem: ContactData, newItem: ContactData): Boolean {
                // 모든 필드가 같은지 확인 (data class의 equals 사용)
                return oldItem == newItem
            }
        }

        const val TYPE_LIST = 0
        const val TYPE_GRID = 1
    }

    override fun getItemViewType(position: Int): Int {
        return viewType
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
        // ViewHolder 생성 로직
        return when (viewType) {
            TYPE_LIST -> ListViewHolder(
                ItemRecyclerViewListBinding.inflate(
                    LayoutInflater.from(
                        parent.context
                    ), parent, false
                )
            )
            TYPE_GRID -> GridViewHolder(
                ItemRecyclerViewGridBinding.inflate(
                    LayoutInflater.from(
                        parent.context
                    ), parent, false
                )
            )
            else -> throw IllegalArgumentException("Invalid view type")
        }
    }

    override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
        // ViewHolder 바인딩 로직
        when (holder.itemViewType) {
            TYPE_LIST -> (holder as ListViewHolder).bind(getItem(position))
            TYPE_GRID -> (holder as GridViewHolder).bind(getItem(position))
        }
    }

	// ViewHolder 구현
    inner class ListViewHolder(private val binding: ItemRecyclerViewListBinding) :
        RecyclerView.ViewHolder(binding.root) {
        fun bind(contact: ContactData) {
            /* 아이템 바인딩 로직 */
        }
    }

    inner class GridViewHolder(private val binding: ItemRecyclerViewGridBinding) :
        RecyclerView.ViewHolder(binding.root) {
        fun bind(contact: ContactData) {
            /* 아이템 바인딩 로직 */
        }
}

 

위 코드는 연락처 앱의 어댑터를 구성한 코드이다. 2개의 뷰 타입을 볼 수 있게 하기 위해 ListViewHolder와 GridViewHolder를 생성한 점을 제외하면 일반 리스트어댑터의 구성과 동일하다.

 

ListAdapter를 상속받아서 그 안에 데이터와 뷰 홀더를 넣어준다. 기존에 RecyclerViewAdapter에서는 인자로 dataList를 받아왔지만 ListAdapter는 변경이 있을 때 submitList를 통해 리스트를 받아오기에 인자가 필요없다.

 

내부적으로 getItem 메소드를 통해서 ListAdapter의 currentList를 받아와 RecyclerViewAdapter의 mItems[position].Item 을 대체한다. 

 

어댑터 구현이 완료되면 어댑터를 세팅하고 어댑터에 데이터를 넣을 때는아래와 같다.

object ContactManager {
	val contactList: MutableList<ContactData> = mutableListOf()
    fun getList(): List<ContactData> {
            return contactList.toList()
        }
}

contactAdapter.submitList(ContactManager.getList())

 

 

중요한 점은 기존의 contactList는 MutableList이지만 호출할 때는 toList()로 변환하여 List형태로 submit한다는 점이다.

ListAdapter의 DiffUtil 이 받아오는 리스트의 주소값이 같으면 동작을 안하기 때문에 toList로 변환하여서 주소값을 변경시켜주어서 다른 리스트라는 것을 알려주어야 한다.

+ Recent posts