0. RecyclerView에서 Data Set을 하는 방법들
RecyclerView의 data set의 변경방식에 관해서는 여러 메서드들과 제공하는 클래스가 있다. DiffUtil이 만능인지 그렇다면 정확히 어떤 면에서 만능인지, 그렇지 않다면 그 외의 것들 중에서는 어떤 것을 써야 효율적이고 적재적소에 쓰는 것이 맞는지 생각해보는 글이 되겠다.
1. DiffUtil은 Infinite Scroll에 맞는 방식인가?
first items convert to second items
스크롤을 하면서 다음 데이터를 계속해서 추가해야하는 무한 스크롤에서 DiffUtil을 사용한다면 기존에 호출한 20개의 아이템에 다시 20개를 추가한 40개의 새 아이템 리스트를 DiffUtil 함수가 포함된 데이터 업데이트 메서드로 보낸다.
DiffUtil.Callback 클래스를 상속받아서 이전 데이터와 새 데이터의 아이템을 비교하여 차이를 계산하는 클래스를 생성하고, 기존 아이템과 새 아이템을 인자로 넣어준다. 그리고 DiffUtil.calculateDiff() 메소드를 호출하여 이전 데이터와 새 데이터간의 차이를 계산한다.
이전 데이터 리스트를 모두 삭제한 후 새 데이터 리스트를 이전 데이터 리스트에 추가하면서 업데이트된 데이터로 채워진 것을 볼 수 있다. first items convert to second items 개념이기에 이전 데이터들을 모두 clear 한 후 새 데이터 리스트들로 바꿔준다. 그리고 dispatch 함수에서 전 데이터를 기억하고 비교해서 갱신해준다.
fun updateItems(newItems: List<ViewItem>) {
val diffCallback = DiffUtilCallBack(items, newItems)
//DiffUtil.Callback 클래스를 상속받아서 이전 데이터와 새 데이터의 아이템을 비교하여 차이를 계산한다.
val diffResult = DiffUtil.calculateDiff(diffCallback)
//DiffUtil.calculateDiff() 메소드를 호출하여 이전 데이터와 새 데이터간의 차이를 계산한다.
items.clear()
//이전 데이터 리스트를 모두 삭제한다.
items.addAll(newItems)
//새 데이터 리스트를 이전 데이터 리스트에 추가하면서 업데이트된 데이터로 채워진다.
diffResult.dispatchUpdatesTo(this)
//DiffResult 객체에 저장된 차이 정보를 RecyclerView 어답터에 적용한다. recyclerview의 아이템을 실제로 업데이트하고 애니메이션을 처리한다.
}
기존 20개의 데이터와 다음 페이지에 대한 새 데이터 20개를 합친 40개의 데이터를 calculateDiff 함수가 비교하여 최종 결과물을 반영한 40개의 아이템을 업데이트해준다는 것. 하지만 기존 데이터에 다음 데이터를 추가를 하면서 전체 데이터를 보여줘야하는 무한 스크롤에서 calculateDiffUtil에게 기대하는 연산은 예상이 되는 동작이다. 단순히 기존 데이터에 새 아이템을 추가하는 연산밖에 필요하지 않다.
그렇다면 notifyInserted 함수를 사용하는 것이 더 효율적이지 않을까?
notifyInserted를 사용하면 새 데이터의 위치에 새로운 아이템을 삽입하고, 기존 데이터와 새 데이터의 전체 목록을 비교하는 것이 아니라 새로운 아이템만을 처리한다. 따라서 두 번째 페이지에서 새로운 20개의 아이템만을 notifyInserted로 처리할 수 있다.
DiffUtil은 전체 데이터 목록을 비교하고 변경 사항을 계산하므로 데이터가 커질수록 더 많은 계산이 필요하고 성능이 저하될 수 있다. 따라서 페이징된 데이터를 처리할 때는 notifyInserted나 notifyItemRangeInserted와 같은 메서드를 사용하여 새로운 아이템만을 업데이트하는 것이 더 효과적일 수 있다.
2. ListAdapter
RecyclerView.Adapter base class for presenting List data in a RecyclerView, including computing diffs between Lists on a background thread.
DiffUtil은 데이터가 커질수록 더 많은 계산이 필요하다는 점이 있는데 ListAdapter은 이에 대한 좋은 대안이 될 수 있다. ListAdapter는 내부적으로 DiffUtil을 활용하여 데이터 변경 사항을 처리하도록 설계되어있다. 그리고 이러한 연산 작업을 background thread 로 처리하고 업데이트된 UI를 main thread로 보내주는 강력한 어답터이다.
3. 연산 vs 렌더링
기기 입장에서는 어떤 것이 더 효율적인 작업인가? 여기서 우리는 연산과 렌더링의 관점으로 나눠서 생각해볼 수 있다.
연산도 효율적이면서 렌더링도 효율적일 순 없다.그렇다면 기계와 사용자 입장에서는 어떤 것이 더 효율적인지를 따져봐야한다. UI를 그려나가는 행위는 CPU를 가장 잡아먹는 것이고 그래서 main thread가 제일 중요하며 이 부분이 관리가 되지 않는다면 앱이 터지기도 한다. 이것이 main thread가 UI 담당만 하는 이유이다.
그래서 렌더링 최적화가 중요한 것이고 이러한 관점에서 컴퓨터가 제일 잘하는 연산, 계산 작업은 뒷단인 background thread에 맡기고 UI를 그리는 작업은 기계 스펙에 달렸기에 어떤 핸드폰 성능에서도 UI가 좋은 품질로 그려지게 하는 것이 중요하다.
그래서 ListAdapter은 이러한 렌더링 측면에서 우수하다. 메인스레드에 작업 많이 남을수록 성능이 올라가고 그래서 앱의 성능을 최적화했다고 볼 수 있는것이다.
notifiyItemChanged를 생각해보자. 한 개 아이템의 한 개의 flag만 바뀌어도 전체 렌더링이 되는 메서드이다. DiffUtil은 이를 보안하기 위해 차이점만을 비교하여 변경해준다는 컨셉으로 나왔다. 하지만 이 계산작업 또한 메인 스레드에서 이루어지기 때문에 이 모든 것을 보안할 수 있는 ListAdapter은 다른 메소드들과는 비교불가한 성능을 가지고 있는 것이다.
4. submitList 함수의 특이한 점
참조 값이 변경되었는지 여부를 확인하여 데이터 변경을 감지
ListAdapter 는 submitList 메서드를 사용하여 데이터를 제출하고 그 뒤에서는 AsyncListDiffer가 동작하여 데이터 변경을 관리한다.
이를 위해 DiffUtil.ItemCallback을 구현하고 현재 리스트에 노출하고 있는 아이템과 새로운 아이템이 서로 같은 지를 비교, 그리고 true라면 아이템의 equals를 비교한다.
companion object {
val diffUtil = object : DiffUtil.ItemCallback<Object>() {
override fun areContentsTheSame(oldItem: Object, newItem: Object) =
oldItem == newItem
override fun areItemsTheSame(oldItem: Object, newItem: Object) =
oldItem.id == newItem.id
}
}
}
ListAdapter의 submitList 함수의 구현체를 보자.
/**
* Submits a new list to be diffed, and displayed.
* <p>
* If a list is already being displayed, a diff will be computed on a background thread, which
* will dispatch Adapter.notifyItem events on the main thread.
*
* @param list The new list to be displayed.
*/
public void submitList(@Nullable List<T> list) {
mDiffer.submitList(list);
}
기존 20개가 담겨져 있는 리스트에 20개를 추가한 40개가 담긴 데이터 리스트를 submitList 함수에 인자로 넣어주었다고 하자. 이전 데이터와의 비교를 해서 갱신해줄 것으로 기대할 것이다. 데이터는 분명 변깅이 되었는데 recyclerview에 갱신이 안되는 경우가 있다.
According to the official docs :
Whenever you call submitList it submits a new list to be diffed and displayed.
This is why whenever you call submitList on the previous (already submitted list), it does not calculate the Diff and does not notify the adapter for change in the dataset.
/**
* Pass a new List to the AdapterHelper. Adapter updates will be computed on a background
* thread.
* <p>
* If a List is already present, a diff will be computed asynchronously on a background thread.
* When the diff is computed, it will be applied (dispatched to the {@link ListUpdateCallback}),
* and the new List will be swapped in.
* <p>
* The commit callback can be used to know when the List is committed, but note that it
* may not be executed. If List B is submitted immediately after List A, and is
* committed directly, the callback associated with List A will not be run.
*
* @param newList The new List.
* @param commitCallback Optional runnable that is executed when the List is committed, if
* it is committed.
*/
@SuppressWarnings("WeakerAccess")
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;
}
...
java에서 == 는 주소값을 비교한다. 즉 새로운 리스트와 기존 리스트가 주소가 같다면 리스트의 데이터가 변경되어도 반영되지 않는다.
주소값이 같은 객체에 데이터만 추가해서 submitList에 넣어주었기 때문에 이전 데이터와 달라진 것이 없다고 인지하는 것이다.
따라서 새로운 객체를 만들던지 주소가 다른 리스트를 넘겨주어야한다.
.observe(this, Observer {
adapter.submitList(it?.toMutableList())
})
주소값이 다른 리스트를 만들어주기 위해 위와 같이 toMutableList()로 리스트를 복사할 수 있다. toMutableList() 함수를 실행했을 때 리스트 자체의 참조값이 달라지므로 submitList 는 이전 데이터와 다른 값임을 인지하게 되는 것이다.
5. .toMutableList()로 알아보는 shallow copy
toMutableList() 함수는 얕은 복사를 수행한다. toMutableList()를 수행했을 시 리스트 자체의 참조값은 달라지지만 요소의 참조값은 같게 된다.
이 함수는 원본 컬렉션의 요소들을 복사하여 새로운 가변(Mutable) 컬렉션에 넣지만, 요소 자체는 복사되지 않고 요소들의 참조가 복사된다. 따라서 새로운 가변 컬렉션과 원본 컬렉션은 같은 요소들을 가리키게 된다.
/**
* Returns a new [MutableList] filled with all elements of this collection.
*/
public fun <T> Collection<T>.toMutableList(): MutableList<T> {
return ArrayList(this)
}
즉, toMutableList()를 사용하면 새로운 리스트 객체가 생성되고, 이 리스트는 원본 리스트와 같은 요소를 참조한다. 따라서 두 리스트의 참조 값은 다르지만 요소들의 참조값은 같다.
따라서 toMutableList()를 사용하여 데이터를 변경하면 새로운 리스트 객체의 참조값이 변경되므로 데이터 변경이 감지되어 UI가 업데이트된다.
'🐸 Android' 카테고리의 다른 글
FCM Notification 2 - background 에서 푸쉬 알람받기 (0) | 2023.12.12 |
---|---|
FCM Notification 1 - 안드로이드 13에서 Notification 권한 허가 변경 (0) | 2023.12.12 |
[Android] Fragment의 LifeCycle과 viewLifecycleOwner 뜯어보기 (0) | 2023.08.13 |
[Android] 안드로이드의 Process와 Thread (0) | 2023.08.09 |
[Android] 안드로이드 로컬 데이터베이스에 데이터 저장 (0) | 2023.08.01 |