RecyclerView.Adapter를 구현한 클래스임.
그럼 왜 쓸까?
만약 RecyclerView.Adapter였다면 비효율적인 notifyItemChanged를 사용하지 않기 위해 어떤 요소들이 바꿨는지 검사하고 추가하는 등 비효율적인 행위들이 많아졌을 것임. 혹은 DiffUtil.Callback을 사용해야했을 것인데, 이것도 사실 코드가 많고 비동기적 처리를 추가적으로 해줘야 좋으므로 실제로 사용하려면 보일러 플레이트 코드가 많이 발생할 것임.
사실 ListAdapter를 먼저 배우고 본다면, DiffUtil이 ListAdapter에서만 사용가능한것처럼 보일 수 있다. 하지만, ListAdapter는 DiffUtil을 활용하는 AsyncListDiffer를 사용해서 편리하게 정의가능한 어댑터일 뿐이다. 일반적인 RecycelrView.Adapter에서도 DiffUtil를 정말 잘 구현한다면 ListAdapter처럼 동작하게 만들 수 있다.
일반적으로 diff 연산에 사용되는 '유진 마이어스 차이 알고리즘'을 사용한다고 함.
AVCABBA -> CBABAC로 바뀌는 경우를 예시로 알고리즘을 이해해보자.
비동기적으로 DiffUtil을 사용할 수 있도록 만들기 쉽게 만들어 놓은 어댑터임.
DiffUtil.Callback을 그대로 사용하지 않고 대신, DiffUtil.ItemCallback을 사용해서 만들어진 AsyncListDiffer를 활용한다.
ListAdapter를 만들기 위해서는, 생성자에 다음 둘 중 한가지를 넘겨주면 된다
둘 중 무엇을 넘겨주던지 간에, 이것들을 활용해서 AsyncListDiffer를 만들어서 ListAdapter의 mDiffer를 초기화 시켜줘야 한다.
=> 별도의 스레드 풀을 지정하고 싶은게 아니면 그냥 DiffUtil.ItemCallback을 넣어주면 된다.
AsyncListDiffer를 만들기 위한 인자로는 AdapterListUpdateCallback과 AsyncDifferConfig 이다.
만약 ListAdapter에 DiffUtil.ItemCallback을 넘겨준다면. 위의 코드에서 보이는 것처럼, 빌더를 활용해서 AsyncDifferConfig 객체를 만들고 이걸로 AsyncListDiffer를 생성한다.
즉, Diff비교 실행이 진행될 백그라운드 작업 스레드 풀을 따로 지정하고 싶은게 아니라면, 그냥 DiffUtil로 ListAdapter를 만들어주면 된다.
AsyncListDiffer.submitList() 가 호출되면,백그라운드 쓰레드에서 DiffUtil.calculateDiff() 함수가 호출되는데, 이때 이 함수의 파라미터로 드디어 우리가 ListAdapter 의 생성자로 넘긴 DiffUtil.ItemCallback 이 활용된다. 그대로 사용되지는 않고 DiffUtil.Callback객체의 내부에서 활용된다.
이 말이 이해가지 않는다면, 아래 코드를 참고해보고 맨 아래의 결론 파트를 보면 좋을 것 같다. 최대한, 이해하는데 불필요한 로직은 제거했다.
public void submitList(@Nullable final List<T> newList,
@Nullable final Runnable commitCallback) {
...
if (newList == mList) { // 껍데기가 같으면 submitList로 교체가 안되는 이유
if (commitCallback != null) {
commitCallback.run();
}
return;
}
final List<T> previousList = mReadOnlyList;
...
final List<T> oldList = mList;
// 작업스레드풀에서 DiffUtil.DiffResult를 얻고 이 결과는 메인스레드에서 처리함.
// DiffResult는 두 데이터 세트 간 차이점을 나타내는 객체임.
// 또한,우리가 정의한 DiffUtil.ItemCallback의 함수를 사용해서
// DiffUtil.Callback 익명객체를 다시 정의하는 것을 알 수 있다.
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;
}
throw new AssertionError();
}
...
});
mMainThreadExecutor.execute(new Runnable() {
@Override
public void run() {
if (mMaxScheduledGeneration == runGeneration) {
latchList(newList, result, commitCallback);
}
}
});
}
});
}
@SuppressWarnings("WeakerAccess") /* synthetic access */
void latchList(
@NonNull List<T> newList,
@NonNull DiffUtil.DiffResult diffResult,
@Nullable Runnable commitCallback) {
...
// 얻은 차이점 결과를 토대로 최소한의 연산으로 데이터를 교체함.
diffResult.dispatchUpdatesTo(mUpdateCallback);
onCurrentListChanged(previousList, commitCallback);
}
private void onCurrentListChanged(@NonNull List<T> previousList,
@Nullable Runnable commitCallback) {
// current list is always mReadOnlyList
for (ListListener<T> listener : mListeners) {
listener.onCurrentListChanged(previousList, mReadOnlyList);
}
if (commitCallback != null) {
commitCallback.run();
}
}
=> DiffUtil.ItemCallback을 오버라이드할 때 이것만 잘 준수해서 해주고, 새 리스트를 제출할때 컬렉션 참조 객체만 동일하게 하지 않으면 사용하면서 문제될 일은 거의 없다.
ListAdapter는 DiffUtil.Callback을 비동기적으로 사용하기 쉽게 만들어놓은 어댑터이며, ListAdapter는 AsyncListDiffer를 소유하고 있고 그 안에는 AdapterListUpdateCallback와 AsyncDifferConfig가 있다. AsyncDifferConfig는 DiffReult연산 결과를 얻는데 사용되며 비동기적으로 수행된다. 그리고 AdapterListUpdateCallback은 실질적으로 그 차이점 결과를 토대로 아이템 변경 메소드를 호출한다.
첫 블로그 글이라 엉성한 부분이 많이 보이는 것 같다. 블로그에 올리는게 확실히 책임감이 있어서 더 개념을 정립하는데 도움이 되는 것 같다. 쓰다보면 더 잘 쓰게 될 것이라 믿으며 마무리한다.