본문 바로가기

안드로이드

Diffutil 에 대한 정리

diffutill은 예전에 솝트 과제에서 사용했었지만 그때는 코드를 긁어와서 넣기만했지 이해도는 거의 없었다.

사실근데 따지고들면 코드 그냥 긁어와서 사용하는게 맞기는하다 그래도 어느정도는 이해가 필요하고 변형을 해야하기 때문에 정리해보겠다.

 

우선 이론적인걸 조금 집고 넘어가보자

diffutil은 RecyclerView에 표현할 데이터를 업데이트하기 위해 주로  사용하는 notifyDataSetChanged()를 대체하기위해서 사용하는것이다.

notifyDataSetChanged()를 사용하면,

Adapter에게 RecyclerView의 데이터가 바뀌었으니 모든 항목을 통째로 업데이트를 하라는 신호를 보낸다.

이 방법은 모든 데이터를 다시 그리기 때문에 굉장히 비효율적이다.

고로 변경된 데이터에 한해서만 adapter를 변경할수있도록 고안한것이 diffutil 클래스이다.

쓰잘데기는 없지만 Eugene W. Myers’s의 차이 알고리즘을 이용한 것이라고한다.

 

우선 diffutil을 사용하는 방법이 3가지 정도있다 

1.diffutil(기본)

2.AsyncListDiffer(백그라운드 스레드에서처리)

3.ListAdapter(결과적으로 이것만 사용하게됨)

 

 

그래서 일단 각각 사용법을 다 알아야하므로 순서대로 살펴보겠다

 

1.DiffUtil

diffutil을 사용하려면 우선 diffutill클래스를 작성해줘야한는데 예시와 함께 봐보자

따로 util에 DiffUtil 클래스를 하나만들어준다(네이밍은 맞춰서해준다)

package changhwan.experiment.sopthomework

import androidx.recyclerview.widget.DiffUtil

class FollowerDiffUtil(private val oldList: List<FollowerData>, private val currentList: List<FollowerData>):
    DiffUtil.Callback(){
    override fun getOldListSize(): Int =oldList.size

    override fun getNewListSize(): Int =currentList.size

    override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
        //원래 id를 많이 쓰는데 여기서는 id값이 아이템 별로 없으므로 followerName으로 대체했다 각각 맞춰서 사용하자
        return oldList[oldItemPosition].followerName==currentList[newItemPosition].followerName
    }

    override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
        return oldList[oldItemPosition]==currentList[newItemPosition]
    }

}

자 이렇게 클래스명은 사실 제멋대로 짓지만 diffutil마다 다루는 리스트의 자료형이 다를거기에 

그에맞춰서 네이밍 해주면 편할것이다.

그리고diffUtil.Callback()를 상속받는데 

diffUtil.Callback()은 추상 클래스이며 4가지 추상메서드와 1개의 비추상 메서드로 이루어져있다

  • getOldListSize(): 이전 목록의 개수를 반환합니다.
  • getNewListSize(): 새로운 목록의 개수를 반환합니다.
  • areItemsTheSame(int oldItemPosition, int newItemPosition): 두 객체가 같은 항목인지 여부를 결정합니다.
  • areContentsTheSame(int oldItemPosition, int newItemPosition): 두 항목의 데이터가 같은지 여부를 결정합니다. areItemsTheSame()이 true를 반환하는 경우에만 호출됩니다.
  • getChangePayload(int oldItemPosition, int newItemPosition): 만약 areItemTheSame()이 true를 반환하고 areContentsTheSame()이 false를 반환하면 이 메서드가 호출되어 변경 내용에 대한 페이로드를 가져옵니다.

이렇게 설명을 적어놨지만 결국에는 결과적으로 옛날리스트와 최신 리스트를 비교한다는 내용이다 그리고 

위의 예시처럼 오버라이딩해주면된다.

빨간색으로 표시해놓은 추상메서드 4개를 필수적으로 오버라이딩 해줘야한다

한가지 주의 점은 areItemsTheSame에서 리스트의 원소들의 고유값으로 쓸만한 것들을 비교시켜야한다

원래 id같은걸 많이하는데 예시에서는 id값이 없어서 followerName로 대체해줬다.

 

이렇게 클래스 작성해준이후 

diffutil을 적용해줄 리사이클러뷰의 어댑터로가서

class FollowerAdapter(private val listener: ItemDragListener) :
    RecyclerView.Adapter<FollowerAdapter.FollowerViewHolder>(), ItemActionListener {

    var followerData = mutableListOf<FollowerData>()

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): FollowerViewHolder {
        val binding =
            FollowerItemBinding.inflate(LayoutInflater.from(parent.context), parent, false)
        return FollowerViewHolder(binding, listener)
    }

    override fun onBindViewHolder(holder: FollowerViewHolder, position: Int) {
        holder.onBind(followerData[position])
    }

    override fun getItemCount(): Int = followerData.size

    //diffUtill 부분 
    fun replaceItems(newUserList: List<FollowerData>) {
        val diffResult =
            DiffUtil.calculateDiff(FollowerDiffUtil(followerData, newUserList))

        followerData.clear()
        followerData.addAll(newUserList)

        diffResult.dispatchUpdatesTo(this)
    }
  
//뷰홀더는 itemdecoration같은게 붙어있어 너무 복잡해서 생략

이렇게 adapter에 함수를 하나 만들어서 최신화된 리스트를 매개변수로 받아서

Diffutil.calculateDiff에다가 작성했던 diffutil 객체를 기존에 적용되어있던 list와 새로운list를 매개변수로

전달해서 변수에 담아준다.(어댑터 내에있기에 어댑터안에 있는 기존 데이터를 전달해주면 될것이다)

 

그후 그결과 값에 대해 dispatcheUpdatesTo()를 호출해주는데 인자로 this를 넣어주는데 adapter를 넣어주어야하므로

adapter내에서 작성해줬기에 this를 넣어준거고 이코드를 밖으로 빼서 매번 사용처에서 적용시키고 싶다면 맞는 adapter을 전달해주면 될것이다.

 

class FollowerFragment : Fragment(), ItemDragListener {

    private var _binding: FragmentFollowerBinding? = null
    private val binding get() = _binding!!

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

    fun siteFollowerRecycler() {
        followerAdapter = FollowerAdapter(this)

        binding.followerRecycle.adapter = followerAdapter

        followerAdapter.followerData.clear()

        followerAdapter.followerData.addAll(
            gitHubViewModel.conclusionData
        )

        //diffUtill부분 원래는 followerAdapter.notifyDataSetChanged()였음
        followerAdapter.replaceItems(gitHubViewModel.conclusionData)
    }

 

프래그먼트에서 쓰잘떼기 없는거 빼서 오히려 어지러워 보이는데

결과적으로

사용처에서 notifyDataSetChanged() 대신 아까 만들어놓은 함수 replaceItems()에다가 새로 적용하고 싶은 리스트를 넣어서 호출해주면 다른부분만 비교해서 새로그려준다.

그래서 이코드가 호출되면 실제로 바뀐내용이 실시간으로 최신화되어서 화면에 바로 새롭게 그려진다.

 

-> 근데 이렇게 사용했던게 과제에 사용했던 diffutil이다 이러면 문제가 한가지있는데 그문제점은 목록이 크면 이작업에 상당한 시간이 걸릴수있다.

그래서 백그라운드 스레드에서 이작업을 실행하여 얻은 DiffUtil.DiffResult 가져와서 기본스레드에서 리사이클러뷰에 적용하느니 이런 스레드 관련 머리아픈걸 해야하는데

이런걸 자동으로 백그라운드로 처리해주고 리스트까지 업데이트 해주는게 AsyncListDiffer이다 이걸 사용한다면 스레드를 아예 신경안쓰고 DiffUtil을 사용할수있다.

(하지만 결국 결론에서도 나오겠지만 이거도 불편하고 귀찮아서 ListAdapter쓰게됨)

 


 

2.AsyncListDiffer

위에서도 설명했듯이 AsyncListDiffer를 사용한다면 스레드를 신경안써도 저절로 백그라운드 스레드에서 처리하기에 만약 listAdapter가 아닌 recyclerViewAdapter와 사용하고싶다면 이 AsyncListDiffer를 사용해야 listAdpater와 같은 성능을 낼수있다. 이제 사용법을 봐보자.

class FollowerDiffItemCallback : DiffUtil.ItemCallback<FollowerData>() {
    override fun areItemsTheSame(oldItem:FollowerData, newItem: FollowerData) =
        oldItem.followerName == newItem.followerName

    override fun areContentsTheSame(oldItem:FollowerData, newItem:FollowerData) =
        oldItem == newItem
}

우선 DiffUtil.ItemCallback()을 상속 받는 class를 작성해주자 네이밍도 위의DiffUtil과 비슷하게 다루는 리스트의 자료형에 맞춰서 사용한다면 편할것이다.

 

이렇게 두가지 함수를 오버라이딩해주고 난후 

 

 

이 AsyncListDiffer를 사용할 adpater를 작성해야한다.

class FollowerAdapter : RecyclerView.Adapter<FollowerViewHolder>() {
    private val asyncDiffer = AsyncListDiffer(this, FollowerDiffItemCallback())

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = FollowerViewHolder(
        ItemUserBinding.inflate(LayoutInflater.from(parent.context), parent, false)
    )

    override fun onBindViewHolder(holder: FollowerViewHolder, position: Int) =
        holder.bind(asyncDiffer.currentList[position])

    override fun getItemCount() = asyncDiffer.currentList.size

    fun replaceItems(newList: List<Follower>) {
        asyncDiffer.submitList(newList)
    }
}

이제 이 어댑터를 자세히 살펴보자면

AsyncListDiffer 객체를 만들는 코드가 있는데 이때 첫번째 인자로는 사용할 adpter와 두번째 인자로는 아까작성한 DiffItemCallback객체를 전달한다

 

기존에는 어댑터내에서

var followerData = mutableListOf<FollowerData>()

이런식으로 변수를 선언해놓고 거기에 list를 넣고 최신화 시켜서 사용했다면

 

이제는 리스트자체가 AsyncListDiffer객체에 기본적으로 내장되어있다고 생각하면된다. 그래서 기본적으로 내가 작성한 DiffItemCallback 에서지정한 item의 자료형을 가진 리스트가 이미 있고 그 리스트는 자체적으로 생성되며

 

그것을 가져오려면(get) asyncDiffer.currentList로 자체적으로 가지고있는 리스트를 꺼내오고

리스트를 업데이트(set)하려면 asyncDiffer.submitList(새로 바꿔줄 리스트)를 통해서 새롭게 리스트를 업데이트 시켜야한다.

 

참고사항으로는

AsyncListDiffer에서 넘어오는 currentList는 READ ONLY 리스트로 변경이 불가능하기 때문에 currentList의 아이템의 변경은 submitList()를 통해서만 가능하다.

 

그래서 결론적으로 어댑터의 바뀐점을 뜯어가며 살펴보자면

onBindViewHolder에서 currentList를 통해서 갖고있는 리스트를 가져와서 사용한다

 

그리고

getItemCount도 같은이유로 currentList를 통해서 리스트를 가져온다.

 

그리고 notifyDataSetChanged()와 같은 역할을 할수있는 함수 replaceItems 는 submitList를 통해서 내장된 list를 업데이트 시킨다

 

그래서 사용법은 notifyDataSetChanged() 사용하는곳에 replaceItems를 호출해주면된다(새로운 정보넣어서)

 

3.ListAdapter

리스트 어댑터는 AsyncListDiffer를 더 쓰기 편하도록 랩핑한 클래스이다.

결론적으로 RecyclerViewAdapter에 AsyncListDiffer를 내장시키도록 확장한것과 같다(listAdapter는 RecyclerViewAdapter를 상속받고있다)

 

결론적으로 ListAdapter를 사용하면 AsyncListDiffer객체를 생성할 필요없이 내장되어있는 Differ를 사용해서 백그라운드 스레드에서 DiffUtill비교연산을 수행할수있다

 

그에관련된 내용을 자세히 살펴보자면

public abstract class ListAdapter<T, VH extends RecyclerView.ViewHolder>
        extends RecyclerView.Adapter<VH> {
    final AsyncListDiffer<T> mDiffer;
    private final AsyncListDiffer.ListListener<T> mListener =
            new AsyncListDiffer.ListListener<T>() {
        @Override
        public void onCurrentListChanged(
                @NonNull List<T> previousList, @NonNull List<T> currentList) {
            ListAdapter.this.onCurrentListChanged(previousList, currentList);
        }
    };

리스트 어댑터의 코드이다 자세히 살펴보면

ListAdapter는 기본적으로 AsyncListDiffer 타입의 mDiffer를 포함하고있다.

그래서 mDiffer를 통해서 AsyncListDiffer객체를 만들지않고 그 동작을 수행할수있다.

 

그래서 사용한 예시를 살펴보자

(멀티뷰타입까지 적용되어있어서 살짝 복잡해보일수있다)

 

package com.spark.android.ui.home.adapter

class HomeRecyclerViewAdapter :
    ListAdapter<Room, RecyclerView.ViewHolder>(homeDiffUtil) {

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
        return when (viewType) {
            TICKET_STARTED -> {
                itemHomeRecyclerviewBinding = ItemHomeRecyclerviewBinding.inflate(
                    LayoutInflater.from(parent.context),
                    parent,
                    false
                )
                HomeRecyclerViewHolder(itemHomeRecyclerviewBinding)
            }
            TICKET_WAITING -> {
                itemHomeRecyclerviewWaitingBinding = ItemHomeRecyclerviewWaitingBinding.inflate(
                    LayoutInflater.from(parent.context),
                    parent,
                    false
                )
                HomeRecyclerViewWaitingHolder(itemHomeRecyclerviewWaitingBinding)
            }
            else -> {
                throw RuntimeException("알 수 없는 viewType error")
            }
        }
    }

    override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
        if (holder is HomeRecyclerViewHolder) {
            holder.onBind(getItem(position))
        } else if (holder is HomeRecyclerViewWaitingHolder) {
            holder.onBind(getItem(position))
        }
    }

    fun updateHomeList(rooms: List<Room>) {
        submitList(rooms)
    }

    companion object {
        private val homeDiffUtil = object : DiffUtil.ItemCallback<Room>() {
            override fun areItemsTheSame(oldItem: Room, newItem: Room): Boolean =
                oldItem.roomId == newItem.roomId

            override fun areContentsTheSame(oldItem: Room, newItem: Room): Boolean =
                oldItem == newItem
        }
    }
}

 

자 코드를 보면 멀티뷰타입때문에 좀 복잡해보이는데 그런거 다 제외시키고 필요한 부분만 보자

 

결론적으로 ListAdpater에서 강제되는 오버라이딩해야하는 함수는

onCreateViewHolder와 onBindViewHolder 만 있다 기존 리사이클러 뷰처럼 getIemCount같은거 안해줘도된다

그리고 뷰홀더는 복잡해서 예제에서는 생략했다-> 필요에 따라서 알아서 만들어서 사용하자

 

천천히 하나하나 뜯어보자면

일단 ListAdapter는 AsyncListDiffer를 내장하고 있다고 생각하면 됨으로 companion object에서 만든 DiffUtillItemCallback에 넣어준 자료형의 리스트를 가지고있다고 생각하면된다.

 

그래서 어댑터 내에 우선 companion object에다 DiffUtil.ItemCallBack를 오버라이딩한 객체를 만들어준다 이때 내가 다루려는 자료형으로 필요한 자리마다 넣어주면된다.

 

그리고 기존에  RecyclerViewAdapter를 상속받아서 만들던것을 ListAdpater를 상속 받아서 만드는데

제너릭에 첫번째는 내가 다루려는 리스트의 아이템의 자료형 두번째는 뷰홀더가 들어가면된다(원래 작성한 뷰홀더가 들어가는데 예제에서는 멀티뷰타입이라 모든 뷰홀더를 받을수있는 RecyclerView.ViewHolder가 사용된것이다)

그리고 아까 companion object에서 작성한 diffutill 객체를 넣어준다.

 

그리고 onCreateViewHolder는 기존과 동일하게 작성해주면된다 딱히 달라지는건 없다.

 

 

onBindViewHolder에서는 원래는 만들어둔 리스트를 사용했지만 내장되어있는 리스트의 아이템을 꺼내쓰기 때문에

내장되어있는 함수인  getItem 을 사용해서 position에 맞는 리스트의 원소를 넣어주면된다.

 

  getItem(position: Int) : ListAdapter 내부 List Indexing을 할 때 활용된다.

->(리스트의 포지션에 맞는 아이템 하나를 꺼내오는것)

  getCurrentList() : ListAdapter가 가지고 있는 리스트를 가져올 때 사용한다.

  submitList(MutableList<T> list) : 리스트 항목을 변경하고 싶을 때 사용한다.

 

함수는 이렇게 있는데 적절하게 사용하면된다.

 

 

그리고 마지막으로 notifyDataSetChanged() 를 대체할수있는 함수를 submitList() 를 통해서 만들고

notifyDataSetChanged() 를 사용해야하는 시점에 새리스트를 넣어서 호출해주면된다.

 

 

 

 

결론적으로 diffutil을 다 살펴봤는데

 

RecyclerviewAdapter와 AsyncListDiffer를 조합해서 사용하던지 

ListAdapter를 사용하면 같은성능을 가진 diffutil을 사용할수있게된다.

-> 난 간단한 listAdapter를 사용할거같다

 


참고블로그

https://voiddani.tistory.com/7

 

[Android] RecyclerView DiffUtil

RecyclerView에 표현할 데이터를 업데이트하기 위해 주로 notifyDataSetChanged()를 호출한다. notifyDataSetChanged() 리스트의 내용이 변경되어 notifyDataSetChanged()를 호출하면, Adapter에게 RecyclerView..

voiddani.tistory.com

https://hungseong.tistory.com/24

 

[Android, Kotlin] RecyclerView의 성능 개선, DiffUtil과 ListAdapter

RecyclerView의 Adapter는 RecyclerView에서 다음과 같은 역할을 한다. 데이터 리스트를 관리하여 포지션에 맞게 ViewHolder의 View와 연결하여 표시하는 중간자 기존 RecyclerView.Adapter를 사용할 경우 위 역할..

hungseong.tistory.com