정리 배경
최근 프로젝트를 진행하다 ListAdapter를 활용해 데이터가 추가되었을 때 RecyclerView가 갱신되는 화면을 구현했습니다. 하지만 submitList를 호출해줬지만 화면이 갱신되지 않아 어려움을 겪었는데 그 이유를 분석해보려 합니다.
Layout의 경우 RecyclerView의 사이즈 변경에 대비해 wrap_content를 설정해준 모습입니다. 버튼 클릭 시 데이터를 submitList를 통해 adapter로 전달하고 있습니다.
문제 당시의 간략한 UI 구성은 다음과 같습니다.
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<androidx.appcompat.widget.AppCompatButton
android:id="@+id/btn_test_update"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Update" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/test_rv"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
</LinearLayout>
</layout>
문제점 분석
1. RecyclerView.Adapter와 ListAdapter 비교하기
일단 이 경우를 파악하기 위해 기존에 잘 동작했던 RecyclerView.Adapter와 ListAdapter를 비교해보기로 했습니다.
기존 RecyclerView.Adapter는 데이터가 들어왔을 때 notifyDataSetChanged를 호출했었고 이 경우엔 화면 갱신이 잘 되는 것을 볼 수 있었습니다.
조금 더 쉽게 비교해보고자 하나의 화면을 분할해서 왼쪽은 RecyclerView.Adapter, 오른쪽은 ListAdapter로 테스트 진행했습니다.
영상에서 보시는 것과 같이 확연한 차이를 볼 수 있습니다.
그렇다면 왜 이럴까 고민을 해보니 문득 submitList의 Data 변경 감지 방식과 제가 Recycler.Adapter에서 설정한 NotifyDataSetChanged가 차이가 있을 것이라는 생각을 하게 되었습니다.
2. ListAdapter 동작 방식 파악하기
먼저 submitList에 대해 깊게 들어가 보기로 했습니다.
submitList은 총 2개의 매개변수를 받을 수 있습니다.
(newList: 대체할 객체, commitCallback: 함수 수행 후 사용자가 원하는 행동 실행)
이제 매개변수를 파악했으니 내부 동작을 하나씩 파악하도록 하겠습니다.
1) newList와 mList 비교하기
저희가 submitList를 호출하게 되면 AsyncListDiffer의 내부에서 기존 리스트와 새로운 리스트를 비교하게 됩니다.
- mList: 기존에 가지고 있던 객체
2) previousList에 mReadOnlyList를 저장
mReadOnlyList는 초기에 emptyList로 지정되어 있으며 추후 동작에서 사용됩니다.
3) newList가 null일 경우 - fast simple remove all
주석에 작성되어있든 새로운 객체를 null로 지정하면 빠르게 기존 데이터들을 제거할 수 있습니다.
- onCurrentListChanged를 통해 기존 매개변수에서 받은 commitCallback이 실행됩니다.
4) mList가 null일 경우 - fast simple first insert
mList가 null인 경우 기존에 AsyncListDiffer에 객체를 주입한 적이 없다는 뜻이므로 새로 들어온 객체를 바로 insert하는 부분입니다.
5) AsnycDifferConfig.getBackgroundThreadExecutor가 호출됩니다.
AsnycDifferConfig는 내부 설명에 의하면 다음과 같습니다.
Configuration object for {@link ListAdapter}, {@link AsyncListDiffer}, and similar * background-thread list diffing adapter logic.
* diffing: compare (files) in order to determine how or whether they differ.
list의 차이를 비교하는 adapter 로직을 가진 ListAdapter, AsyncListDiffer, 그리고 유사한 백그라운드 스레드의 구성 객체
At minimum, defines item diffing behavior with a {@link DiffUtil.ItemCallback}, used to compute * item differences to pass to a RecyclerView adapter.
최소한 {@linkDiffUtil}을(를) 사용하여 item의 diffing behavior 을 정의합니다. ItemCallback}은(는) RecyclerView 어댑터에 전달할 항목 차이를 계산하는 데 사용됩니다.
oldList에 기존에 가지고 있던 리스트를 저장하고 mConfig(AsnycDifferConfig)의 getBackgroundThreadExecutor를 통해 비동기 처리를 진행합니다. (ListAdapter의 submit이 비동기인 이유입니다)
내부적으로 DiffUtil의 calculateDiff는 세부적인 리스트 비교 로직이기 때문에 넘어가겠습니다.
6) calculateDiff가 종료되면 마지막으로 latchList를 호출하게 됩니다.
latchList 내부에서 mList를 newList로 대체하고 dispatchUpdatesTo 함수를 호출합니다.
7) dispatchUpdatesTo 내부에서 계산이 되면서 데이터를 관리합니다.
하지만 내부 코드를 확인해봐도 requestLayout이 존재하지 않습니다. 따라서 submitList가 호출되어도 Layout이 재할당되지 않는 것입니다.
3. NotifyDataSetChanged 파악하기
1) notifyDataSetChanged()가 호출되면 mObservable의 notifyChanged() 가 호출됩니다.
여기서 mObservable는 AdapterDataObservable의 객체입니다.
2) notifyChanged()에서 내부 mObservers 객체들의 onChange를 호출합니다.
여기서 내부 객체들은 RecyclerView의 추상 클래스인 AdapterDataObserver를 상속받은 RecyclerViewDataObserver로 이뤄져있습니다. onChanged가 호출되면 아래 코드 동작이 진행됩니다.
requsetLayout이 호출하면 새롭게 그리면서 성능상 문제가 발생하기 때문에 최후의 수단으로 호출하라고 Android에서 명시하는 것입니다.
결론
notifyDataSetChanged는 requestLayout을 통해 새롭게 View를 그리면서 공간을 재할당받는 것입니다.
이 외의 것들은 requestLayout이 없기 때문에 기존의 영역에서 업데이트되는 것을 볼 수 있었습니다.
즉, NotifyDataSetChanged를 쓰지 않고 싶다면 RecyclerView의 영역을 wrap_content가 아닌 적당한 공간을 할당해주는 것도 좋은 방법일 수 있습니다.
'Android' 카테고리의 다른 글
[Android] SharedPreference commit()과 apply() 중 어떤 걸 써야할까? (0) | 2023.05.23 |
---|---|
Android Dokka를 통한 KDoc 문서화하기 (0) | 2023.04.30 |
[Android] Toml은 무엇인가요? - Gradle Version 관리 (0) | 2023.04.12 |
[Android] Sticky Header 만들기 (0) | 2023.04.09 |
[번역] Android Development | Best Practices | by JGeun (0) | 2023.03.05 |
댓글