notifyDataSetChanged()
notifyDataSetChanged()
은 호출하면 requestLayout()
을 통해 새롭게 View를 그리면서 공간을 재할당받기 때문에 비용이 큰 작업이다.
DiffUtil
- RecyclerView에 존재하는 기존 아이템(oldList)들과 새로운 아이템(newList)들의 차이를 계산해서 반영하는 유틸리티 클래스이다.
- Eugene W. Myers 알고리즘을 사용하여 최소 업데이트 횟수를 계산한다.
- 사용을 위해서는 DiffUtil.Callback()을 상속받은 클래스를 구현해야한다.
class MatchDiffUtilCallBack(
private val oldMatches: List<MatchUiModel>,
private val newMatches: List<MatchUiModel>
) : DiffUtil.Callback() {
override fun getOldListSize(): Int = oldMatches.size
override fun getNewListSize(): Int = newMatches.size
override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean =
oldMatches[oldItemPosition] == newMatches[newItemPosition]
override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean =
oldMatches[oldItemPosition] == newMatches[newItemPosition]
}
areItemsTheSame()
: 두 항목이 같은 객체인지 반환한다.
areContentsTheSame()
: 두 항목의 데이터가 같은지 여부를 결정한다. areItemsTheSame()
이 true인 경우에만 호출된다.
class RecentMatchAdapter2(private var matches: List<MatchUiModel>) : RecyclerView.Adapter<MatchViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MatchViewHolder {
return MatchViewHolder.valueOf(parent)
}
override fun getItemCount(): Int = matches.size
override fun onBindViewHolder(holder: MatchViewHolder, position: Int) {
holder.bind(matches[position])
}
fun updateMatches(matches: List<MatchUiModel>) {
val diffCallback = MatchDiffUtilCallBack(this.matches, matches)
val diffResult: DiffUtil.DiffResult = DiffUtil.calculateDiff(diffCallback)
this.matches = matches
diffResult.dispatchUpdatesTo(this)
}
}
- 데이터가 많아질수록 알고리즘의 시간복잡도가 커지기 때문에 백그라운드 스레드에서 처리하는 것이 좋다.
AsyncListDiffer
- AsyncListDiff는 백그라운드 스레드에서 리스트의 변경사항을 계산하고 업데이트까지 진행을 시켜준다.
- DiffUtil.ItemCallback를 구현하는 클래스를 만들어야 한다.
class MatchDiffUtilCallBack : DiffUtil.ItemCallback<MatchUiModel>() {
override fun areItemsTheSame(
oldItem: MatchUiModel,
newItem: MatchUiModel
): Boolean = oldItem == newItem
override fun areContentsTheSame(
oldItem: MatchUiModel,
newItem: MatchUiModel
): Boolean = oldItem == newItem
}
areItemsTheSame()
: 두 항목이 같은 객체인지 반환한다.
areContentsTheSame()
: 두 항목의 데이터가 같은지 여부를 결정한다. areItemsTheSame()
이 true인 경우에만 호출된다.
- AsyncListDiffer는 newList와 oldList의 참조가 같은 경우 변경사항을 체크하지도 않고 종료한다.
- 새로운 데이터를 대입할 때 새로운 리스트를 생성한 후
submitList()
를 하면 문제가 되지 않지만, 기존의 리스트를 수정해 submitList()
를 하게 되면 같은 참조값으로 인해 리스트가 업데이트 되지 않는 버그가 발생한다.
public void submitList(@Nullable final List<T> newList,
@Nullable final Runnable commitCallback) {
final int runGeneration = ++mMaxScheduledGeneration;
if (newList == mList) {
if (commitCallback != null) {
commitCallback.run();
}
return;
}
...
}
class RecentMatchAdapter2(private var matches: List<MatchUiModel>) : RecyclerView.Adapter<MatchViewHolder>() {
private val asyncDiffer = AsyncListDiffer(this, MatchDiffUtilCallBack())
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MatchViewHolder {
return MatchViewHolder.valueOf(parent)
}
override fun getItemCount(): Int = matches.size
override fun onBindViewHolder(holder: MatchViewHolder, position: Int) {
holder.bind(matches[position])
}
fun updateMatches(matches: List<MatchUiModel>) {
asyncDiffer.submitList(matches)
}
}
ListAdapter
- AsnycListDiffer를 내부적으로 사용하는 클래스이다.
private val asyncDiffer = AsyncListDiffer(this, MatchDiffUtilCallBack())
다음과 같은 코드를 작성할 필요가 없다.
- AsyncListDiffer와 마찬가지로
DiffUtil.ItemCallBack
클래스를 사용한다.
class RecentMatchAdapter : ListAdapter<MatchUiModel, MatchViewHolder>(MatchDiffUtilCallBack()) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MatchViewHolder {
return MatchViewHolder.valueOf(parent)
}
override fun onBindViewHolder(holder: MatchViewHolder, position: Int) {
holder.bind(getItem(position))
}
override fun submitList(list: List<MatchUiModel>?) {
super.submitList(list)
}
}