요즘 RecyclerView를 위한 Adpater를 구현할 때는 주로 RecyclerView.Adapter보단 ListAdapter를 상속받아 구현한다.
ListAdapter는 androidx.recyclerview 패키지에 소속되어 있고 RecyclerView.Adapter를 확장한 클래스로 AsyncListDiffer를 통해 리스트 데이터를 비교하여 반영한다.
ListAdapter<T, VH>의 생김새를 알아보자.
public abstract class ListAdapter<T, VH extends RecyclerView.ViewHolder>
extends RecyclerView.Adapter<VH> {
final AsyncListDiffer<T> mDiffer;
protected ListAdapter(@NonNull DiffUtil.ItemCallback<T> diffCallback) {
mDiffer = new AsyncListDiffer<>(new AdapterListUpdateCallback(this),
new AsyncDifferConfig.Builder<>(diffCallback).build());
mDiffer.addListListener(mListener);
}
protected ListAdapter(@NonNull AsyncDifferConfig<T> config) {
mDiffer = new AsyncListDiffer<>(new AdapterListUpdateCallback(this), config);
mDiffer.addListListener(mListener);
}
ListAdapter의 생성자를 보면 2개가 있는데 흔히 사용하는 생성자는 DiffUtil.ItemCallback을 넘겨주는 첫 번째 생성자이다.
근데 두번째 생성자의 정체와 AsyncDifferConfig는 뭘까?
AsyncDifferConfig는 내부적으로 main thread executor와 background tread executor 중 별도로 지정하는 경우에 사용하는 객체이다.
즉 특별한 이유로 작업할 스레드 환경을 지정해줄게 아니라면 그냥 DiffUtil.ItemCallback을 넘겨주는 생성자를 사용하면 된다.
DifUtil.ItemCallback만 넘겨주더라도 내부적으론 AsyncListDiffer객체를 생성하면서 인자로 넘겨줄 AsyncDifferConfig 객체를 생성하는데 자체적으로 멀티 쓰레드에 대한 처리가 되어있고 Executors.newFixedThreadPool(2)로 쓰레드 풀을 만들어 background thread에서 사용자가 정의한 리스트 아이템 비교 연산(DiffUtil.Callback)을 처리한다.
@NonNull
public AsyncDifferConfig<T> build() {
if (mBackgroundThreadExecutor == null) {
synchronized (sExecutorLock) {
if (sDiffExecutor == null) {
sDiffExecutor = Executors.newFixedThreadPool(2);
}
}
mBackgroundThreadExecutor = sDiffExecutor;
}
return new AsyncDifferConfig<>(
mMainThreadExecutor,
mBackgroundThreadExecutor,
mDiffCallback);
}
ListAdapter는 AsyncListDiffer를 프로퍼티로 가지고 있고, ListItem 객체 관리나 DiffUtil을 통한 ListItem의 변경사항에 따른 로직 처리 등은 모두 AsyncListDiffer를 통해 이루어 진다.
ListAdapter 구현하기는 애초에 쉽게 활용하라고 만든 클래스이기도 하고, 함수 이름들도 역할이 명확하기 때문에 큰 어려움이 없다.
하지만 DiffUtil.ItemCallback을 구현하여 인자로 넘겨줌으로써 뭔가 ListItem의 변화에 따라 자동으로 리스트의 데이터가 갱신된다고 착각할 수도 있지만 DiffUtil.ItemCallback을 통한 아이템 변경 감지는 리스트의 업데이트를 위해선 명시적으로 submitList()를 호출할 때 수행되기 때문에 리스트를 갱신시키기 위해선 이 점을 분명히 해야 한다.
그럼 ListAdapter에 AsyncListDiffer를 알아보자.
ListAdapter를 생성하면서 넘긴 DiffUtil.ItemCallback 객체는 AsyncListDiffer에서 사용하게 된다.
AsyncListDiffer에는 몇 개의 메소드들이 있지만 핵심 로직인 submitList()만 집중해서 보면 좋을 것 같다.
(나머지는 주석이나 구현내용을 보면 쉽게 이해가능하다)
submitList()를 호출하게 되면 AsyncListDiffer는 이전에 저장하고 있던 리스트를 previous리스트에 보관하고 새로운 리스트들 currentList로 업데이트한다.
이때 newList가 새로 생겼거나 없어진 경우 ListUpdateCallback을 통해 내부적으로 RecyclerView.Adapter의 notifyItemRangeInserted(), notifyItemRangeRemoved() 함수가 호출된다.
이 과정을 거치고 DiffUtil.calculateDiff()를 통해 직접 정의하고 파라미터로 넘긴 DiffUtil.ItemCallback에 의해 내부적으로 리스트 아이템들을 비교하며 리스트를 갱신하게 된다.
말로 설명하기엔 이해에 어려움이 있으니 AsyncListDiffer.java의 원본 코드를 살펴보자.
public void submitList(@Nullable final List<T> newList,
@Nullable final Runnable commitCallback) {
// incrementing generation means any currently-running diffs are discarded when they finish
final int runGeneration = ++mMaxScheduledGeneration;
if (newList == mList) { //새로운 리스트 데이터와 원래의 리스트 데이터가 똑같으면 아무 동작 안함
// nothing to do (Note - still had to inc generation, since may have ongoing work)
if (commitCallback != null) {
commitCallback.run();
}
return;
}
final List<T> previousList = mReadOnlyList;
// fast simple remove all
if (newList == null) { //새로운 리스트가 없는 경우
//noinspection ConstantConditions
int countRemoved = mList.size();
mList = null;
mReadOnlyList = Collections.emptyList(); //최근 리스트 데이터 초기화
// notify last, after list is updated
mUpdateCallback.onRemoved(0, countRemoved); //RecyclerView.Adapter의 notifyItemRangeRemoved()호출
onCurrentListChanged(previousList, commitCallback); //업데이트
return;
}
// fast simple first insert
if (mList == null) { //새로운 리스트가 있는 경우
mList = newList;
mReadOnlyList = Collections.unmodifiableList(newList); //최근 리스트 데이터 갱신
// notify last, after list is updated
mUpdateCallback.onInserted(0, newList.size()); //RecyclerView.Adapter의 notifyItemRangeInserted()호출
onCurrentListChanged(previousList, commitCallback); //업데이트
return;
}
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); //우리가 정의해서 넘긴 areItemsTheSame 수행
}
// 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); //우리가 정의해서 넘긴 areContentsTheSame 수행
}
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);
}
}
});
}
});
}
위 설명한 내용은 이해한 기준으로 작성된 내용이기에 포스팅 진행이 매끄럽지 않을 수 있다.
되도록이면 원본 코드들을 살펴보며 따라가보는 것을 추천한다. 흐름을 이해하기엔 생각보다 어렵지 않다!
'Android' 카테고리의 다른 글
[Android] OS 10 이상에서 알림 갯수 제한 문제 해결 (0) | 2024.06.19 |
---|---|
[Android] DiffUtil과 Payload를 활용한 RecyclerView 성능 최적화 (0) | 2024.06.18 |
[Android] Notification 커스텀하기 (0) | 2022.12.21 |
[Android] Activity와 Fragment (0) | 2022.12.15 |
[Android] URI, URL (0) | 2022.11.24 |