-
DiffUtil, ListAdapter로 RecyclerView 성능 개선안드로이드 2023. 2. 5. 15:22
RecyclerView만을 사용하다보니 하나의 데이터를 변경하는 경우에도 전체 데이터를 리프레쉬 해야하는 불필요한 과정을 겪게 되었습니다. ListAdapter와 DiffUtil을 적용 후 변경이 필요한 코드만 변경할 수 있게 되어 데이터 불러오는 속도를 높일 수 있었습니다.
개요
메모장 앱을 개발하며 이미 저장한 데이터를 수정하는 기능을 구현할 때, 사소한 변경임에도 지금껏 notifyDataSetChanged() 한 줄로 리사이클러뷰를 갱신하고 있었습니다.
fun updateNotes(notesList: List<ANoteEntity>) { this.notesList = notesList notifyDataSetChanged() }이렇게 코드를 작성하게 된다면, 아이템 딱 하나가 바뀌는 상황이더라도 리스트를 지우고 다시 처음부터 끝까지 객체를 하나하나 만들어 새로 렌더링하는 과정을 거치게 됩니다. 때문에 변경하지 않아도 되는 부분까지 변경될 수 있어 비용이 매우 크게 발생합니다.
Recyclerview의 데이터가 변하면 Recyclerview Adapter가 제공하는 notifyItem 메소드를 사용해서 ViewHolder 내용을 갱신할 수 있지만 저는 번거롭게 notify를 주는것 말고 다른 방법을 택했습니다.
notifyItemChanged(int) notifyItemInserted(int) notifyItemRemoved(int) notifyItemRangeChanged(int, int) notifyItemRangeInserted(int, int) notifyItemRangeRemoved(int, int)(사용하기에 따라서는 갱신이 필요없는 ViewHolder를 같이 갱신하는 불필요한 작업이 생길수도 있습니다.)
그러다 알게 된 것이 DiffUtil 클래스입니다.
DiffUtil | Android Developers
androidx.constraintlayout.core.motion.parse
developer.android.com
DiffUtil에 대해 알아보자
DiffUtil은 이전 데이터 상태와 현재 데이터간의 상태 차이를 계산하고, 반드시 업데이트해야 할 최소한의 데이터에 대해서만 갱신합니다. oldItem, newItem의 두 데이터셋을 비교하여 값이 변경된 부분만을 RecyclerView에게 알려주기 때문입니다. 이렇게 된다면 데이터 업데이트 횟수를 최소한으로 가져가 기존 문제였던 필요하지 않았던 부분까지 갱신하는 문제를 해결할 수 있습니다.
DiffUtil만을 사용한다면 getOldListSize, getNewListSize 를 함수를 추가로 재정의 해야하지만, ListAdapter의 DiffUtil은 areItemsTheSame, areContentsTheSame 두가지 만으로도 구현할 수 있습니다.
- getOldListSize : 현재 리스트에 노출하고 있는 List size
- getNewListSize : 새로 추가하거나, 갱신해야 할 List size
저는 ListAdapter와 함께 DiffUtil을 사용하여 아래와 같이 코드를 작성했습니다.
class SharedDiffUtil : DiffUtil.ItemCallback<ViewData>() { override fun areItemsTheSame( oldItem: ViewData, newItem: ViewData, ): Boolean { return oldItem.id == newItem.id } override fun areContentsTheSame( oldItem: ViewData, newItem: ViewData, ): Boolean { return oldItem == newItem } }1. DiffUtil.ItemCallback을 통해 DiffUtil의 객체를 생성합니다.
2. areItemsTheSame 메소드에서는 두 아이템이 같은 객체인지 여부를 확인합니다.- return 값이 false라면, 데이터가 바뀐 것이므로 RecyclerView에 변경을 반영합니다.
- return 값이 true라면 areContentsTheSame 메소드를 호출하여 두 값이 동일한 아이템인지를 확인합니다.
3. areContentsTheSame 메소드에서는 두 아이템이 같은 데이터를 가지고 있는지 여부를 반환합니다.
- areItemsTheSame() 이 true 를 반환할 때만 호출됩니다. (객체가 다르다면 데이터를 비교하는 것은 의미가 없기 때문)
- return 값이 false라면 DiffUtil은 해당 데이터의 변경이 필요하다고 판단하고 RecyclerView에 반영합니다.
- return 값이 true라면 아이템과 데이터 모두 변경이 없는 것이므로 값의 변경을 반영하지 않습니다.
AsyncListDiffer, ListAdapter
DiffUtil을 구현하여 이제 비교 연산으로 필요한 부분만 갱신할 수 있게 되었습니다.
여기서 비교 연산을 백그라운드로 처리하는 작업을 추가로 더 해줘야 합니다.
이유는 리스트 아이템이 많으면 비교 연산을 일일히 수행해야해 작업시간이 길어질 수 있기 때문입니다.
이러한 작업을 쉽게 해주는 것이 AsyncListDiffer입니다.

AsyncListDiffer 백그라운드 스레드에서 DiffUtil을 통해 두 목록 간의 차이를 계산하기 위한 도우미입니다. RecyclerView.Adapter에 연결할 수 있으며 요약된 목록 간의 변경 사항을 어댑터에 알립니다. 간단히 하기 위해 AsyncListDiffer 대신 ListAdapter 래퍼 클래스를 직접 사용할 수 있습니다. 이 AsyncListDiffer는 비동기 목록 diffing을 지원하기 위해 어댑터 기본 클래스를 재정의하는 것이 편리하지 않은 복잡한 경우에 사용할 수 있습니다.AsyncListDiffer의 설명을 보면 AsyncListDiffer를 간단하게 사용할 수 있는 ListAdapter 래퍼 클래스를 제공해준다고 말합니다. ListAdapter는 AsyncListDiffer를 포함하는 클래스로, RecyclerView.Adapter 대신 ListAdapter를 사용함으로써 AsyncListDiffer 객체의 생성 없이도 백그라운드 스레드에서 DiffUtil의 비교 연산을 편하게 수행할 수 있습니다.
그래서 저는 비교연산을 보다 편하게 할 수 있는 ListAdpater를 사용하여 작업을 해주었습니다.
abstract class SharedAdapter : ListAdapter<ViewData, RecyclerView.ViewHolder>(SharedDiffUtil()) { override fun onCreateViewHolder( parent: ViewGroup, viewType: Int, ): RecyclerView.ViewHolder { return SharedViewHolder( LayoutViewHolderBinding.inflate( LayoutInflater.from(parent.context), parent, false ) ) } override fun onBindViewHolder( holder: RecyclerView.ViewHolder, position: Int, ) { if (holder is SharedViewHolder) { val viewData = getItem(position) as ViewData holder.bind(viewData) } } }변경된 list를 asyncListDiffer의 submitList(list) 메소드를 통해 반영해 비교 할 수 있게 되었습니다.
viewModel.searchDataList.observe(viewLifecycleOwner) { searchDataList -> searchAdapter.submitList(searchDataList) }참고
https://developer.android.com/reference/androidx/recyclerview/widget/DiffUtil
https://developer.android.com/reference/androidx/recyclerview/widget/AsyncListDiffer
https://developer.android.com/reference/androidx/recyclerview/widget/ListAdapter
'안드로이드' 카테고리의 다른 글
LiveData에서 Flow로 마이그레이션 (0) 2023.04.17 Retrofit을 사용한 이유를 '깊게' 얘기해보기 (0) 2023.04.11