나만 보는 일기장

[RecyclerView, DiffUtil] RecyclerView와 DiffUtil 쓰는 법 본문

개발/Android

[RecyclerView, DiffUtil] RecyclerView와 DiffUtil 쓰는 법

Patrick0422 2021. 11. 2. 19:53

RecyclerView는 기존의 리스트 뷰와 그리드 뷰 등을 대체하는 새 라이브러리라고 할 수 있는데,

기존의 리스트뷰가 리스트에 들어가 있는 아이템을 모두 만들어 화면에 보이지 않는 부분에도 자원이 낭비되는데 비해,

RecyclerView는 화면에 보이지 않는 부분의 아이템을 이름처럼 재활용해 사용함으로써 자원을 훨씬 효율적으로 사용하게 됩니다.

리스트뷰와 리사이클러뷰의 차이

RecyclerView 구현 방법

  1. 레이아웃에 RecyclerView 추가
  2. RecyclerView 아이템 디자인
  3. RecyclerView Adapter 구현
  4. DiffUtil 구현
  5. RecyclerView에 Layout Manager, Adapter 설정

1. RecyclerView 추가

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools">

    <data>

    </data>

    <androidx.constraintlayout.widget.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context=".MainActivity">

        <androidx.recyclerview.widget.RecyclerView
            android:layout_width="0dp"
            android:layout_height="0dp"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent" />

    </androidx.constraintlayout.widget.ConstraintLayout>
</layout>

따로 종속성을 추가해줄 필요는 없습니다.

2. RecyclerView 아이템 디자인

RecyclerView에 표시될 아이템의 레이아웃을 만들어줍시다.

 

RecyclerView 안에 들어갈 아이템
아이템의 클래스

3. RecyclerView Adapter 구현

ViewAdapter는 다음의 요소들을 가지고 있습니다.

RecyclerView에 표시될 항목들을 저장하는 리스트

private var memberList = emptyList<MemberProfile>()

ViewHolder 클래스

class MemberViewHolder(private var binding: ListItemBinding) : RecyclerView.ViewHolder(binding.root) {
        fun bind(member: MemberProfile) {
            binding.member = member
            binding.executePendingBindings()
        }

        companion object {
            fun from(parent: ViewGroup): MemberViewHolder {
                val layoutInflater = LayoutInflater.from(parent.context)
                val binding = ListItemBinding.inflate(layoutInflater, parent, false)

                return MemberViewHolder(binding)
            }
        }
    }

ViewHolder에는 2번에서 만든 레이아웃에서 자동으로 생성된 바인딩 오브젝트를 매개변수로 넣어줍니다.

 

bind() 함수는 아이템 하나를 매개변수로 받아서 View의 변수에 할당해주는 역할을 합니다.

bind() 함수 안의 executePendingBindings() 함수는 뷰를 업데이트해주는 역할을 합니다.

 

companion object 안의 from() 함수는 ViewHolder 객체를 만들어서 리턴해주는 함수입니다.

 

이런 방식은 강의를 보다가 알게 된 방식인데, ViewHolder 객체는 Adapter의 onCreateViewHolder() 함수에서 필요로 하는 것이기 때문에 onCreateViewHolder() 함수에서 ViewHolder 객체를 만든다면 이 from() 함수는 없어도 되는 존재지만 ViewHolder 안에 코드를 옮김으로써 전체적으로는 코드가 더 깔끔해지는 느낌을 받아서 쓰게 되었습니다.

class MemberViewHolder(private var binding: ListItemBinding) :
        RecyclerView.ViewHolder(binding.root) {
        fun bind(member: MemberProfile) {
            binding.member = member
            binding.executePendingBindings()
        }
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MemberViewHolder {
        val layoutInflater = LayoutInflater.from(parent.context)
        val binding = ListItemBinding.inflate(layoutInflater, parent, false)

        return MemberViewHolder(binding)
    }

▲ onCreateViewHolder()에서 ViewHolder 객체를 만드는 방식

class MemberViewHolder(private var binding: ListItemBinding) :
        RecyclerView.ViewHolder(binding.root) {
        fun bind(member: MemberProfile) {
            binding.member = member
            binding.executePendingBindings()
        }

        companion object {
            fun from(parent: ViewGroup): MemberViewHolder {
                val layoutInflater = LayoutInflater.from(parent.context)
                val binding = ListItemBinding.inflate(layoutInflater, parent, false)

                return MemberViewHolder(binding)
            }
        }
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MemberViewHolder =
        MemberViewHolder.from(parent)

▲ ViewHolder 클래스 내부에 from() 함수에서 ViewHolder 객체를 만드는 방식

필수적으로 구현해야 하는 3개의 함수

override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MemberViewHolder =
    MemberViewHolder.from(parent)

override fun onBindViewHolder(holder: MemberViewHolder, position: Int) {
    holder.bind(memberList[position])
}

override fun getItemCount(): Int = memberList.size

onCreateViewHolder() 함수에서는 ViewHolder 객체를 만들어 리턴해 주어야 하는데, 아까 ViewHolder 클래스에서 from() 함수를 만들었기 때문에 한 줄로 짧게 쓸 수 있습니다.

 

onBindViewHolder() 함수는 RecyclerView가 주어진 위치에 정보를 표시할 때 호출하는 함수로, 똑같이 ViewHolder 클래스에서 bind() 함수를 만들어 처리했기 때문에 그냥 bind() 함수를 호출해주면 됩니다.

 

getItemCount() 함수는 1번에서 만든 리스트의 크기 값을 전달해주면 됩니다.

리스트를 설정할 때 쓸 함수

fun setData(newMemberList: List<MemberProfile>) {
        memberList = newMemberList
        notifyDataSetChanged()
    }

RecyclerView에 표시될 항목들을 업데이트할 때 호출할 함수입니다.

여기서 RecyclerView에게 항목이 업데이트되었음을 알리기 위해  notifyDataSetChanged() 함수를 호출하게 되는데, 

notifyDataSetChanged() 호출시 나오는 경고문

보면 notifyDataSetChanged()를 호출하기보다는 다른 방법을 쓸 것을 권장하고 있는데, 이는 notifyDataSetChanged() 함수의 작동 방식이 리스트 전체를 업데이트하는 방식이기 때문에, 리스트의 아이템 일부만 변경된 경우 등에는 이 함수를 호출하는 것이 비효율적이기 때문입니다.

 

그래서 notifyDataSetChanged() 함수 대신 DiffUtil을 사용하는 것인데, DiffUtil은 아래에서 적용해보도록 하겠습니다..

Adapter 전체 코드

class MemberViewAdapter: RecyclerView.Adapter<MemberViewAdapter.MemberViewHolder>() {
    private var memberList = emptyList<MemberProfile>()

    class MemberViewHolder(private var binding: ListItemBinding) : RecyclerView.ViewHolder(binding.root) {
        fun bind(member: MemberProfile) {
            binding.member = member
            binding.executePendingBindings()
        }

        companion object {
            fun from(parent: ViewGroup): MemberViewHolder {
                val layoutInflater = LayoutInflater.from(parent.context)
                val binding = ListItemBinding.inflate(layoutInflater, parent, false)

                return MemberViewHolder(binding)
            }
        }
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MemberViewHolder = MemberViewHolder.from(parent)

    override fun onBindViewHolder(holder: MemberViewHolder, position: Int) {
        holder.bind(memberList[position])
    }

    override fun getItemCount(): Int = memberList.size

    fun setData(newMemberList: List<MemberProfile>) {
        memberList = newMemberList
        notifyDataSetChanged()
    }
}

4. DiffUtil 구현

위에 나온 notifyDataSetChanged() 함수의 문제점 때문에 사용하는 것이 DiffUtil입니다.

DiffUtil은 원래 리스트를 새로운 리스트와 비교하고, 바뀐 부분만 업데이트할 수 있게 해주는 라이브러리입니다.

 

class MemberDiffUtil(
    private val oldList: List<MemberProfile>,
    private val newList: List<MemberProfile>
): DiffUtil.Callback() {
    override fun getOldListSize(): Int {
        TODO("Not yet implemented")
    }

    override fun getNewListSize(): Int {
        TODO("Not yet implemented")
    }

    override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
        TODO("Not yet implemented")
    }

    override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
        TODO("Not yet implemented")
    }

}

DiffUtil 클래스는 기본적으로 DiffUtil.Callback()을 상속받고, 기존의 리스트와 새로운 리스트 두 개를 인자로 받습니다.

그리고 getOldListSize(), getNewListSize(), areItemsTheSame(), areContentsTheSame() 4개의 함수를 구현해야 합니다.

override fun getOldListSize(): Int = oldList.size

override fun getNewListSize(): Int = newList.size

getOldListSize(), getNewListSize() 함수는 간단히 기존 리스트와 새 리스트의 크기를 반환해주면 됩니다.

 

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

override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean =
    oldList[oldItemPosition] == newList[newItemPosition]

areItemsTheSame() 함수는 DiffUtil에서 호출되어 인자로 들어오는 값을 통해 이전 리스트의 아이템과 현재 리스트의 아이템이 동일한지 판단하는 함수입니다.

 

areContentsTheSame() 함수는 areItemsTheSame() 함수에서 True 값이 들어온 경우에만 호출되며, 두 아이템이 같은 데이터를 가지고 있는지 판단하는 함수라고 합니다.

 

 

참고

=== 연산자는 동일성을 비교하는 연산자로, 두 연산 대상이 완전히 같은지 비교합니다. (메모리 주소가 같은지)

== 연산자는 동등성을 비교하는 연산자로, 두 연산 대상이 가지고 있는 값이 같은지 비교를 한다고 알고 있으면 될 것 같습니다.

 

코틀린(Kotlin) | 동등성 vs 동일성

동일성 및 동등성의 의미 동일성 (equality) 두개의 오브젝트가 완전히 동일한 것을 의미한다. 하나의 오브젝트만 존재하는것이며 그 오브젝트를 참조하는 여러개의 레퍼런스 변수를 갖고 있는것

jinn-blog.tistory.com

자세한 내용은 위 블로그를 보면 될 것 같습니다.

 

 

아무튼 여기까지 하면 DiffUtil의 구현이 끝납니다. 이제 적용을 해봅시다.

fun setData(newMemberList: List<MemberProfile>) {
    val memberDiffUtil = MemberDiffUtil(memberList, newMemberList)
    val diffUtilResult = DiffUtil.calculateDiff(memberDiffUtil)

    memberList = newMemberList

    diffUtilResult.dispatchUpdatesTo(this)
}

아까 만들었던 RecyclerView Adapter의 setData() 함수를 위와 같이 변경해주면 됩니다.

 

먼저 memberDiffUtil에 방금 만든 DiffUtil 객체를 넣어주고, diffUtilResult에는 DiffUtil.calculateDiff() 함수의 결괏값을 넣어주면 됩니다. 

 

그 후 현재 리스트를 새로 들어온 리스트로 바꿔주고 dispatchUpdatesTo() 함수를 호출해 주면 DiffUtil 설정이 끝나게 됩니다.

5. RecyclerView에 Layout Manager, Adapter 설정

private val mAdapter by lazy { MemberAdapter() }

...

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    
    ...

    setUpRecyclerView()
}

private fun setUpRecyclerView() {
    binding.recyclerView.adapter = mAdapter
    binding.recyclerView.layoutManager = LinearLayoutManager(this)
}

RecyclerView의 설정은 보통 이렇게 함수로 빼서 onCreate()나 onCreateView()에서 호출해주면 됩니다.

 

Adapter 같은 경우에는 리스트의 데이터셋을 바꾸는 경우 등에 다른 함수에서도 호출하므로 함수 밖으로 꺼내놓았습니다.

 

layoutManager의 경우에는 여러 가지가 있는데, 보통의 리스트뷰를 만들기 위해서 LinearLayoutManager을 적용해주었습니다.

여러 LayoutManager들

그 이후 어댑터에서 만들었던 setData() 함수에 리스트를 넘겨주면 끝~~~

Comments