-
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