RecyclerView를 사용할 때 보통 RecyclerView.Adapter를 상속하여 Adapter를 만들어준다. 그리고 RecyclerVie.Adatper가 제공하는 notifyItemXXX(), notifyDataSetChanged() 메소드를 이용해서 데이터가 변경되었음을 RecyclerView에게 notify한다.
그런데 notifyItemXXX()을 사용하여 notify 하려면 데이터가 변경되었는지, 추가되었는지, 제거되었는지를 일일이 확인해야 한다는 번거로움이 생긴다. 또한 notifyDataSetChanged()를 호출하면 모든 아이템을 새로 그리게 되기 때문에, 갱신이 필요없는 아이템을 같이 갱신하는 불필요한 작업이 생길 수 있다.
RecyclerView.Adapter를 사용하는 대신 ListAdapter를 사용하면 이러한 문제점들을 쉽게 해결할 수 있다.
DiffUtil은 두 리스트 간의 차이를 계산하고, 이전 리스트에서 새로운 리스트로 변환하는 업데이트 작업 목록을 산출하는 utility class이다.
AsyncListDiffer는 백그라운드 스레드에서 DiffUtil class를 통해 두 리스트 간의 차이를 계산하는 Helper class이다.
RecyclerView.Adapter를 상속하는 class로 ListAdapter는 AsyncListDiffer를 더 편하게 사용하기 위한 Wrapper class이다. submitList()로 새로운 리스트를 argument로 넘겨주면 내부적으로 백그라운드 스레드에서 이전 리스트와의 차이를 계산해서 알아서 업데이트한다.
class ImagesAdapter : ListAdapter<Document, ItemSearchedViewHolder>(diffCallback) {
private var dataSet: List<Document> = listOf()
//... (onCreateViewHolder 등의 메서드 생략)
companion object {
private val diffCallback: DiffUtil.ItemCallback<Document> = object : DiffUtil.ItemCallback<Document>() {
override fun areItemsTheSame(oldDocument: Document, newDocument: Document): Boolean {
return oldDocument.thumbnailUrl == newDocument.thumbnailUrl
}
override fun areContentsTheSame(oldDocument: Document, newDocument: Document): Boolean {
return oldDocument == newDocument
}
}
}
fun setData(dataSet: List<Document>) {
this.dataSet = dataSet
submitList(dataSet)
}
//...
}
위의 코드는 ListAdapter를 상속한 Adapter 예시 코드이다.
DiffUtil.ItemCallback class를 상속하여 메서드를 override한다. DiffUtil.Callback class는 리스트 인덱싱(indexing), 아이템 비교(diffing) 두가지 역할을 수행하는데 효율적으로 아이템 비교만 처리하기 위해서 DiffUtil.ItemCallback class를 사용한다. 리스트 인덱싱은 AsyncListDiffer의 submitList()에서 처리해주고 있다. 이따가 submitList() 과정을 따라가보면서 좀더 설명하겠다.
areItemsTheSame() : 두 아이템이 같은 아이템인지 체크할 때 호출된다. 그래서 아이템의 고유한 값을 비교해야 한다.areContentsTheSame() : 두 아이템이 같은 데이터를 가지고 있는지 체크할 때 호출된다. 이 메서드는 areItemsTheSame() 메서드가 true일 때만 호출된다. public void submitList(@Nullable final List<T> newList,
@Nullable final Runnable commitCallback) {
// ...
final List<T> oldList = mList;
mConfig.getBackgroundThreadExecutor().execute(new Runnable() {
@Override
public void run() {
final DiffUtil.DiffResult result = DiffUtil.calculateDiff(new DiffUtil.Callback() {
@Override
public int getOldListSize() {
return oldList.size();
}
@Override
public int getNewListSize() {
return newList.size();
}
@Override
public boolean areItemsTheSame(int oldItemPosition, int newItemPosition) {
T oldItem = oldList.get(oldItemPosition);
T newItem = newList.get(newItemPosition);
if (oldItem != null && newItem != null) {
return mConfig.getDiffCallback().areItemsTheSame(oldItem, newItem);
}
// If both items are null we consider them the same.
return oldItem == null && newItem == null;
}
@Override
public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) {
T oldItem = oldList.get(oldItemPosition);
T newItem = newList.get(newItemPosition);
if (oldItem != null && newItem != null) {
return mConfig.getDiffCallback().areContentsTheSame(oldItem, newItem);
}
if (oldItem == null && newItem == null) {
return true;
}
// There is an implementation bug if we reach this point. Per the docs, this
// method should only be invoked when areItemsTheSame returns true. That
// only occurs when both items are non-null or both are null and both of
// those cases are handled above.
throw new AssertionError();
}
@Nullable
@Override
public Object getChangePayload(int oldItemPosition, int newItemPosition) {
T oldItem = oldList.get(oldItemPosition);
T newItem = newList.get(newItemPosition);
if (oldItem != null && newItem != null) {
return mConfig.getDiffCallback().getChangePayload(oldItem, newItem);
}
// There is an implementation bug if we reach this point. Per the docs, this
// method should only be invoked when areItemsTheSame returns true AND
// areContentsTheSame returns false. That only occurs when both items are
// non-null which is the only case handled above.
throw new AssertionError();
}
});
mMainThreadExecutor.execute(new Runnable() {
@Override
public void run() {
if (mMaxScheduledGeneration == runGeneration) {
latchList(newList, result, commitCallback);
}
}
});
}
});
}
ImagesAdapter의 submitList()를 따라가다 보면 결국 AsyncListDiffer.java의 submitList()에 도달한다.
먼저 mConfig.getBackgroundThreadExecutor().execute(new Runnable() { 부분을 통해서 백그라운드 스레드에서 동작한다는 것을 알 수 있다.
그리고 DiffUtil.Callback() 익명 객체가 보이는데, override된 areItemsTheSame()과 areContentsTheSame()에서
T oldItem = oldList.get(oldItemPosition);
T newItem = newList.get(newItemPosition);
위와 같이 리스트 인덱싱을 해주는 것을 볼 수 있다.
if (oldItem != null && newItem != null) {
return mConfig.getDiffCallback().areItemsTheSame(oldItem, newItem);
}
//...
if (oldItem != null && newItem != null) {
return mConfig.getDiffCallback().areContentsTheSame(oldItem, newItem);
}
그리고 위와 같이 내부에서 또다른 areItemsTheSame()과 areContentsTheSame() 메서드가 있는 것을 볼 수 있는데 이 메서드들이 DiffUtil.ItemCallback class의 메서드들이다. 이렇게 리스트 인덱싱을 AsyncListDiffer에서 내부적으로 처리해주고 있기 때문에 우리는 우리가 만든 Adapter(여기서는 ImagesAdapter)에서 DiffUtil.ItemCallback의 메서드들만 override해주면 되는 것이다.
그리고 맨 아래 메인 스레드에서 latchList() 메서드가 호출되었는데 이 메서드의 선언을 살펴보면 아래와 같다.
void latchList(
@NonNull List<T> newList,
@NonNull DiffUtil.DiffResult diffResult,
@Nullable Runnable commitCallback) {
final List<T> previousList = mReadOnlyList;
mList = newList;
// notify last, after list is updated
mReadOnlyList = Collections.unmodifiableList(newList);
diffResult.dispatchUpdatesTo(mUpdateCallback);
onCurrentListChanged(previousList, commitCallback);
}
이 메서드에서 dispatchUpdatesTo() 메서드를 주목해야 한다. dispatchUpdatesTo(@NonNull ListUpdateCallback updateCallback) 메서드는, 설명에 따르면 주어진 Callback 클래스에 업데이트 작업 목록을 전달(Disptach)하는 메서드이다. 그리고 여기서 사용된 mUpdateCallback는 AsyncListDiffer class의 프로퍼티(멤버 변수)이다.
근데 업데이트 작업 목록은 내가 만든 ImagesAdapter에 전달되어야 하는데, mUpdateCallback에 어떻게 내가 만든 ImagesAdapter 정보가 포함되어 있는 것일까 궁금했다.
@SuppressWarnings("unused")
protected ListAdapter(@NonNull DiffUtil.ItemCallback<T> diffCallback) {
mDiffer = new AsyncListDiffer<>(new AdapterListUpdateCallback(this),
new AsyncDifferConfig.Builder<>(diffCallback).build());
mDiffer.addListListener(mListener);
}
그 답은 ListAdapter의 생성자에서 발견할 수 있었다. 여기서 new AdapterListUpdateCallback(this)의 this는 디버깅으로 확인해보니 내가 만든 ImagesAdapter이다. AdapterListUpdateCallback은 업데이트 이벤트 목록을 주어진 adapter에 전달하는 class이다.(ListUpdateCallback interface를 구현하는 class이다.)
그리고 이렇게 만들어진 객체는,
public AsyncListDiffer(@NonNull ListUpdateCallback listUpdateCallback,
@NonNull AsyncDifferConfig<T> config) {
mUpdateCallback = listUpdateCallback;
mConfig = config;
if (config.getMainThreadExecutor() != null) {
mMainThreadExecutor = config.getMainThreadExecutor();
} else {
mMainThreadExecutor = sMainThreadExecutor;
}
}
AsyncListDiffer 생성자에서 볼 수 있듯이 AsyncListDiffer 인스턴스를 초기화할 때 argument로 전달된다. 그리고 아까 봤던 mUpdateCallback을 전달된 listUpdateCallback으로 초기화한다.
이를 통해 diffResult.dispatchUpdatesTo(mUpdateCallback);를 호출할 때, mUpdateCallback는 이미 ImagesAdapter를 가지고 있는 AdapterListUpdateCallback 객체로 초기화되어 있는 상태라는 것을 알 수 있었다.
지금까지 ListAdapter의 핵심 class와 submitList()를 호출했을 때 업데이트 작업(update operations) 목록이 내가 만든 Adapter에 전달되기까지의 내부 과정을 살펴보았다.
https://developer.android.com/reference/androidx/recyclerview/widget/DiffUtil
https://developer.android.com/reference/androidx/recyclerview/widget/DiffUtil.ItemCallback
https://developer.android.com/reference/androidx/recyclerview/widget/AsyncListDiffer
https://developer.android.com/reference/kotlin/androidx/recyclerview/widget/ListAdapter