반응형
이어지는 글
RecyclerView.ViewHolder에서의 ViewTreeLifecycleOwner 사용
참고: https://pluu.github.io/blog/android/2021/09/20/lifecycleowner/
기존 ViewTreeLifecycleOwner 사용시
- 기존
onBindViewHolder
에서 위와 같이null
이 반환되는 것을 볼 수 있다.
ViewTreeLifecycleOwner의 간단한 개념
Android
에는 생명주기 개념을 가지는 Component로Activity
/Fragment
가 있다.
그리고, 생명주기를 다룬다는 정의를 LifecycleOwner 인터페이스를 통해서 선언하고 있다.
public interface *LifecycleOwner* {
@NonNullLifecycle getLifecycle();
}
// 출처: https://github.com/androidx/androidx/blob/androidx-main/lifecycle/lifecycle-common/src/main/java/androidx/lifecycle/LifecycleOwner.java
AndroidX
에는ComponentActivity
와Fragment
에서LifecycleOwner interface
를 구현하고 있다.
그래서, 해당Component
에서는 직접getLifecycle()
함수를 통해서 직접 생명주기를 다룰 수 있다.
코드 분리
- 리팩토링 등으로
Activity
/Fragment
에 코드는 없지만,Activity
/Fragment
의 LifecycleOwner 인터페이스에 접근해야 할 경우가 있다.
→ 이때 유용한 API가 바로ViewTreeLifecycleOwner
- 그리고,
LifecycleOwner
은 ViewTreeLifecycleOwner.get 혹은 View.findViewTreeLifecycleOwner KTX를 통해서 가져올 수 있다.
public fun View.findViewTreeLifecycleOwner(): LifecycleOwner? = ViewTreeLifecycleOwner.get(this)
// 출처: https://github.com/androidx/androidx/blob/androidx-main/lifecycle/lifecycle-runtime-ktx/src/main/java/androidx/lifecycle/View.kt
RecyclerView.ViewHolder에서의 LifecycleOwner를 사용할 위치
Lifecycle
에 맞춘 핸들링은Activity
/Fragment
뿐만 아니라 경우에 따라서View
가 존재하는 어느 곳이더라도 필요할 수도 있다.- 또한,
View.findViewTreeLifecycleOwner
KTX를 사용하면 쉽게LifecycleOwner
를 가져올 수 있다는 것을 이미 알고 있다. onBindViewHolder
에서도ViewHolder
의itemView
를 사용하면 당연히LifecycleOwner
가 반환될 것이라고 기대하지만, 위에서 봤다시피 결과는null
을 반환한다.- 그 이유가 무엇일까?
1. ViewTreeLifecycleOwner
- 이를 위해 먼저 살펴볼 것은,
ViewTreeLifecycleOwner.get
이다.findViewTreeLifecycleOwner
KTX 또한ViewTreeLifecycleOwner.get
을 호출하고 있으므로LifecycleOwner
를 가져오는 핵심 코드라고 볼 수 있다.
public class ViewTreeLifecycleOwner { @Nullable public static LifecycleOwner get(@NonNull View view) { LifecycleOwner found = (LifecycleOwner) view.getTag(R.id.view_tree_lifecycle_owner); if (found != null) return found; ViewParent parent = view.getParent(); while (found == null && parent instanceof View) { // View#getParent가 유효한지 체크 final View parentView = (View) parent; found = (LifecycleOwner) parentView.getTag(R.id.view_tree_lifecycle_owner); parent = parentView.getParent(); } return found; } }
→ 기본 동작은View.getParent
를 반복적으로 호출하면서LifecycleOwner
가 존재하는지 체크하는 형태이다- 이어서
onBindViewHolder
에서View.getTag
와View.getParent
를 확인해 보면
→ 당연히 View.getTag를 통한 LifecycleOwner는 null이다.
→ 그러나,ViewHolder
에서View
에 해당하는itemView
의 parent는null
이라고 출력된다.ViewTreeLifecycleOwner#get
로직에서 내부적으로View.getParent
를 호출하면서LifecycleOwner
가 존재하는 객체를 찾지만, 여기에서는parent
가null
이므로 최종적으로null
로 반환되었던 것이다.
→null
이 반환되는 원인은ViewHolder
가 가리키는 View의부모 View가 null
이기 때문이었다.
2. RecyclerView 생명주기
- 위에서 살펴봤듯이,
RecyclerView
사용 시에onCreateViewHolder
를 통해서View
가 생성되고,onBindViewHolder
를 통해서 값을 반영하는 코드라는 것을 알고 있다. - 그런데,
onBindViewHolder
단계에서ViewHolder
의View
는RecyclerView
에 Add되었을까?ViewHolder
의itemView
는Attach
되었을까?
→ 정답은 No이다. 이전 RecyclerView Deep Dive - 1. RecyclerView 정의와 동작원리 및 생명주기 글에서의 RecyclerViewㅢ Birth의 요약을 보면, 최초ViewHolder
를 생성하는 경우, Bind한 뒤에, 마지막 단계에addView()
를 하고,onViewAttachedToWindow()
를 호출하는 것을 볼 수 있다.
→ 즉, ViewHolder
는 onBindViewHolder
시점이 되더라도 RecyclerView
에 add 되지 않은 상태이기에, . onBindViewHolder
에서 parent
가 null
로 반환됐던 것이며, findViewTreeLifecycleOwner
결과가 null
로 나온 것이다.
3. onViewAttachedToWindow
- 그렇다면,
onViewAttachedToWindow
이후엔 View가 add되었다 볼 수 있다. 그렇다면 결과가 달라질 수 있을까? - 출처:https://pluu.github.io/blog/android/2021/09/20/lifecycleowner/
-
(RecyclerView의 child로 add 됨)
ViewHolder
의itemView
의parent
가RecyclerView
로 지정된다 parent
가 존재하므로findViewTreeLifecycleOwner
API 결과로 유효한LifecycleOwner
가 반환된다
-
→ 따라서 LifecycleOwner를 사용하기 위해선 View가 Attach된 이후에 사용해야 한다
RecyclerView에서 LifecycleOwner의 사용
RecyclerView.ViewHolder
를 통한LifecycleOwner
탐색 시 2가지 사실을 알 수 있었다.ViewTreeLifecycleOwner.get
에서parent
를 반복적으로 호출하면서LifecycleOwner
를 소유하는 객체를 찾는다onBindViewHolder
단계에서는RecyclerView
의 child로 추가되지 않는다
LifecycleOwner
를 얻는 시점을onViewAttachedToWindow
에서 다룰 수도 있지만,
매번Adapter
에서ViewHolder
에LifecycleOwner
를 주입하는 코드는 놓치기 쉬운 코드며 모든ViewHolder
에서는 필요한 경우는 드물다.- 대신
ViewHolder
의 초기화 단계(init)에서doOnAttach
KTX 함수를 사용하면ViewHolder
에서 코드를 작성할 수 있다.
→ 결과로 간단하게View
가Attach
된 이후에ViewTreeLifecycleOwner.get
API를 사용할 수 있다.
class ViewHolder(
private val binding: ItemRecyclerViewSampleBinding
) : RecyclerView.ViewHolder(binding.root) {
private var lifecycleOwner: LifecycleOwner? = null
init {
itemView.doOnAttach {
lifecycleOwner = itemView.findViewTreeLifecycleOwner()
}
itemView.doOnDetach {
lifecycleOwner = null
}
}
}
그 결과
Tips & Tricks
No Update == No OnBind
- 아이템을 업데이트하지 않는다면 → 그 아이템에 대한
onBind
는 호출되지 않는다.
즉, 아이템이 새로운 아이템으로 업데이트 되지 않는다면onBind
는 재호출되지 않는다.
❌ Don’t this (잘못된 코드)
mAdapter.notifyItemMoved(1, 5);
// onBind 호출 X
// onBind가 일어나지 않으므로 invalidate도 없다.
public void onBindViewHolder(final ViewHolder holder, final int position) {
holder.itemView.setOnClickListener(
new View.OnClickLister() {
@Override
public void onClick(View v) {
removeAtPosition(**position**);
}
}
}
}
// 아이템이 이동되었을 때 position을 가지고 뭔가 추가하거나 제거하려는 동작을 구현하였으나
// 아래 코드 상황에서 position이 올바르게 동작하지 않는 버그가 발생한다.
onBindViewHolder(holder, 5);
notifyItemMoved(5, 15);
holder.itemView.callOnClick();
// 버그 발생!
✅ Do this
public void onBindViewHolder(final ViewHolder holder, final int position) {
holder.itemView.setOnClickListener(
new View.OnClickLister() {
@Override
public void onClick(View v) {
removeAtPosition(**holder.getAdapterPosition**);
}
}
}
}
// ViewHolder는 현재 어댑터의 position을 알고 있기에 getAdapterPosition을
// 사용하면 아이템이 이동되는 이벤트에도 정확한 position을 구할 수 있다.
Payloads
- 아이템의 변경에
Payload
를 사용할 수 있다. - 예를 들어, 유저의 좋아요 이벤트 같은 아이템의 변경에 페이로드를 사용해볼 수 있는데 이는
onBind
를 더 효율적으로 만들며, 더 나은 애니메이션을 제공할 수 있다.
mAdapter.notifyItemChanged(position, LIKE_UPDATE);
// "좋아요" 이벤트가 발생하여 아이템을 업데이트
onBind
에서payloads
가 비었는지를 체크해볼 수 있다.payloads
가 존재한다면 전체 아이템을 전부 다시Bind
할 필요가 없어진다.payloads
를 통해 오직 변경점에 대한 부분만 다시Bind
할 수 있게 되는 것
public void onBindViewHolder(MyViewHolder holder, int position, List payloads) {
if (payloads.isEmpty()) {
// 전체 Re-Bind
onBindViewHolder(holder, position);
} else if (payloads.contains(LIKE_UPDATE)) {
// 부분적인 Re-Bind
Item item = mItems.get(position);
holder.likeButton.setEnabled(!item.isLiked());
}
}
OnCreate
Means “create
”
- 자주하는 실수는 헤더 뷰 같은 것을 구현할 때 발생한다.
아래 코드에서 기존의 헤더ViewHolder
가 존재한다면 재사용해주도록 Return하고 있다.
❌ Don’t this
ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
if (viewType == HeaderViewHolder.TYPE) {
if (mHeaderViewHolder == null) {
mHeaderViewHolder = new HeaderViewHolder(parent);
}
return mHeaderViewHolder;
}
return new MyViewHolder(parent);
}
- 하지만 위 코드는 버그를 발생시킬 수 있는데, 이에 대해
onCreateViewHolder
가 어떤 의미인지 생각해볼 필요가 있다.
말 그대로 ‘생성’을 의미하는 것이기 때문에 위 코드는 바람직하지 않다.
✅ Do this
ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
if (viewType == HeaderViewHolder.TYPE) {
return new HeaderViewHolder(parent);
}
return new MyViewHolder(parent);
}
Positions : Adapter
vs Layout
adapterPosition
과layoutPosition
은 아이템 리스트의 변경이 생겼을 때 차이가 발생하게 된다.
왜냐하면다음 레이아웃 과정
에서 이 position을 계산하는 과정이 비동기적이기 때문이다.- 비로소
레이아웃 과정
을 마치고View
가refresh
된 이후에야adapterPosition
과layoutPosition
이 일치하게 되는 것이다.
→ 그러므로 데이터에 접근할 때
는 adapterPosition
을 사용하는게 좋을 것이고
유저가 무언가를 클릭 했을 때 위, 아래에 있는 아이템에 접근해야할 경우
에는 layoutPosition
을 사용하는게 좋다.
RecyclerView의 최적화
1. RecyclerView/Adapter의 함수를 효율적으로 응용
setHasStableIds
와 getItemId
HasStableIds
사용을 통해, 데이터를 바인딩하는 경우,onBindViewHolder()
를 최적화되게 호출 할 수있다.- 참고로,
DiffUtil
을 사용하는 경우, setHasStableId(true) , getItemId()는 사용하지 않아도 된다.
차이 알고리즘에 의해 필요한 부분만 업데이트가 이미 되기 때문이다.
HasStableIds
사용으로 최적화 호출이 가능한 조건
- 둘 중 하나의 조건에만 해당한다면 성능을 크게 향상시킬 수 있다
- 똑같은 데이터가 반복적으로 나타나는 리스트
notifyDataSetChanged
를 자주 호출한다
최적화 호출 원리
setHasStableIds(true)
를 사용하면 각각 아이템position
에 지정된id
를 기준으로 상황에 따라onBindViewHolder()
호출을 제외시킨다.- 값이 변경된
id
만onBindViewHolder
를 호출하거나 호출된 아이템의id
가 이전position
아이템에 이미 존재할 시onBindViewHolder
함수를 호출 하지 않고 이전에 같은id
를 가진View
를 대신 보여준다. - 이러한 원리로 같은 데이터임을 알고, 데이터 바인드를 할 필요가 없기 때문에 호출되지 않으며 그만큼 성능이 향상될 수 있는 것이다.
사용 방법
HasStableIds
는Adapter.setHasStableIds(boolean)
을 통해 설정할 수 있고, 사용하는 경우Adapter
의getItemId(int)
를 반드시 구현해야 작동한다.getItemId(int)
를 통해 해당 아이템은 고정된 상태로 설정된다.
사용 예시 (https://blog.kmshack.kr/Stable-Id를-이용한-RecyclerView-성능-향상법/)
position | return |
---|---|
0 | 100 |
1 | 200 |
2 | 300 |
3 | 100 |
4 | 400 |
5 | 500 |
- 위와 같이 값이 반환되게 구현된 경우, onBindViewHolder(view, int)는 position 0, 1, 2, 4, 5만 호출된다.
- 3번은 0번째 포지션에서 같은 고정된 ID를 반환했기 때문에 같은 데이터로 인식하여 onBindViewHolder(view, int)가 호출되지 않는다
→ 같은 데이터임을 알고 데이터 바인드를 할 필요가 없기 때문에 호출되지 않으며 그만큼 성능은 향상된다.
position | return |
---|---|
0 | 100 |
1 | 200 |
2 | 600 |
3 | 300 |
4 | 100 |
5 | 400 |
6 | 500 |
- position 2번에 데이터를 추가하고 notifyDataSetChanged()를 호출하였다. 이때, onBindViewHolder(view, int) 는 현재 보이고 있는 포지션이 모두 호출되지만 StableId를 사용하게 된다면 이미 호출된 고정된 ID를 제외한 위치가 호출된다.
- notifyDataSetChanged()를 하였음에도 변경되는 ID만을 골라 해당 포지션만 onBindViewHolder(view, int)를 호출하게 됨으로 그만큼 성능은 향상된다.
setHasFixedSize
RecyclerView
는 데이터가 삽입, 삭제가 될 때 각각 아이템의 레이아웃을 다시 계산해야할지 정한다.hasFixedSize
가true
일 경우 고정값으로 인식하고,false
일 경우requestLayout()
호출이 되어 아이템의 레이아웃 계산이 다시 이루어진다.View
와 관련된 작업은 모두UI Thread
가 위임하기 때문에, 버벅거림이 없는60프레임
을 달성 하려면 최대한UI Thread
를 효율적으로 활용 해야한다.- 따라서 아이템 UI의 사이즈가 고정이라면
true
로 하는게 좋다.
중첩 RecyclerView간의 Pool 공유
- 중첩되어 구현된
RecyclerView
에서RecyclerView
간의pool
를 공유해 성능을 향상 시킬 수 있다.
→ 바깥쪽과 안쪽 pool를 공유하면 된다.
예시
override fun onCreateViewHolder(...): RecyclerView.ViewHolder {
val innerLm = LinearLayoutManager(...) innerRv.apply{
layoutManager = innerLm recyclerViewPool = sharedPool
} return OuterAdapter.ViewHolder(innerRv)
}
2. View
및 onBindViewHolder
최적화
onBindViewHolder
에서 복잡한 로직 피하기
onBindViewHolder
는 가능하면 순수 data set만 하는 것이 가장 이상적이다.data
를 외부에서 미리 가공해서onBindViewHolder
에서set
하는 것이 가장 안정적이다.
구체적 예시
onBindViewHolder
에서 가능하면for
,while
과 같은 반복문은 피한다.onBindViewHolder
에서 특별한 경우를 제외하고 콜백,리스너 정의를 하지 않아야 한다.
(ex: onClickListener)
ovrride fun onBindViewHolder(
holder: RecyclerView.ViewHolder,
position: Int
) {
// 앱 성능 저하의 원인
for (...) {...}
// 불 필요한 코드
view.setOnClickListenr { ... }
//아래 처럼 set만 하는 코드만 존재하는 것이 가장 이상적
imageView.set...(data[position].url)
textView.text = data[position].text
}
ViewHolder 및 onCreateViewHolder에서 가능한 미리 값 처리
예시 코드
- 예시로
width
를 디바이스 기기의 3분의 1 크기로 고정값으로 사용해야하는 경우
class ViewHolder(v: View) :
RecyclerView.ViewHolder(v) {
init {
// 가능하면 미리 정할 수 있는 값은 ViewHolder나 onCreateView에서 처리하는 것이 좋다.
v.layoutParms = LayoutParms(....)
v.itemView.setOnClickListener { ... }
}
override fun onBindViewHolder(...) {
//별로 좋지않는 코드, 성능 저하의 원인 중 일부분이 될 수 있음
holder.layoutParms = LayoutParms(....)
holder.itemView.setOnClickListener { ... }
}
}
→ 위 처럼 가능하면 ViewHolder
에서 미리 처리할 수 있는 것은 set
을 해두는 것이 좋고 onBindViewHolder
에 최대한 부담을 없애야 한다.
OnBindViewHolder
에서 Html.fromHtml()
사용 지양
html
를 사용하기 때문에 마찬가지로onBindViewHolder
에 적합하지 않다.- 성능저하의 원인 중 하나가 된다.
아이템 레이아웃 UI 계층 단순화
안드로이드 xml
은View
가 위치한depth
가 한단계 씩 깊어질 수록UI 연산
이 배로 증가한다
https://android-developers.googleblog.com/2017/08/understanding-performance-benefits-of.html
→ 성능 저하의 원인으로 이어질 수 있다- 따라서, 특별한 경우를 제외하고 가능하면 xml 에서
ConstraintLayout
를 사용해1 depth
로 View를 그리는 것이 좋다- 이상적코드
<android.support.constraint.ConstraintLayout>
<ImageView />
<ImageView />
<TextView />
<EditText />
<TextView />
<TextView />
<EditText />
<Button />
<Button />
<TextView />
</android.support.constraint.ConstraintLayout>
부모 계층 ViewGroup
이 ScrollView
or RecyclerView
recyclerView.setNestedScrollingEnabled(false)
- 상위 뷰에 따라 스크롤이 부드럽지 않는 경우도 있다.
- 그럴 때 위 코드를 적용하면 한층 더 좋아짐을 알 수가 있으며
XML
에서도 설정 가능하다
item
에 복잡한 drawable
사용 지양
<layout-list>
로 이루어진 복잡한drawable
을item XML
에 가능하면 사용하지 말아야한다.
→ 저가용 기기에서 GPU 에 부하를 주기 때문에 가능하면 심플하게 사용해야 한다
ViewStub
사용
ViewStub
으로 지연 인플레이트를 사용하는 것이 좋다.visible
로 설정된 경우에inflate
하기 때문에 성능 컨트롤에 효율적이다.
투명색은 가능하면 지양
- 투명색 사용은 저가용 기기에서 상당한 부하가 될 수 있다.
- 이는 투명색 처리는 연산처리가 많기 때문에 가급적 피하는 것이 좋다.
3. 이외의 시도
LayoutManager
custom을 통해 onCreateView
호출을 여러번 미리 가져오기 (PreCache
)
최적화 원리
ViewHolder
생성 ~ 제거 Lifecycle에서onCreateViewHolder
는 최초로ViewHolder
생성한다.- 하지만 [생성]을 미리 해놓는 경우, 다음 LifeCycle에서
onCreateViewHolder
호출 횟수를 줄이는 방법이 존재한다
→ 바로RecyclerView
가 내부적으로 인식하는 화면 사이즈를 크게 잡는 것 RecyclerView
는사용자가 눈으로 볼 수 있는 화면
+보이지 않는 위아래 가상의 영역의 사이즈
가 있는데 이전체 사이즈
를 내부적으로 늘리면
→ 기본적으로 갖는 아이템의 양의 수보다 더 많은 아이템을onCreateViewHolder
로 생성 해놓을 수 있다.- 스크롤을 하게 되면 이미
onCreateViewHolder
로 생성한 아이템들로 인해onCreateViewHolder
함수의 호출이 적어진다.
class PreCacheLayoutManager(context: Context, private val extraLayoutSpace: Int) :
LinearLayoutManager(context) {
override fun getExtraLayoutSpace(state: RecyclerView.State?) = extraLayoutSpace
}
//Activity 에서..
recyclerView.adapter = PreCacheLayoutManager(context,600)
단점
- 그러나, 사이즈를 너무 크게 잡아버리면
init
시 호출되는onCreateViewHolder
가 많아지고 가상으로 인식하는 사이즈가 매우 넓기 때문에init
타이밍에 버벅거림이 존재할 수 있다. - 따라서, 이 방식은 적재적소하게 사용하는 것이 좋다.
LayoutManager.setItemPrefetchEnabled(true, false)
사용
RecyclerView 25.0.0
부터 기본적으로setItemPrefetchEnabled()
이 true로 설정되어 있다.
→ 따라서true
로 하고 싶다면 별도의 설정을 하지 않아도 된다
최적화 원리
// false 하고 싶을 때만 직접적으로 명시
layoutManager.itemPrefetchEnabled = false
onBindViewHolder
를 몇 프레임 앞서서 미리 호출하는 방식이다.
→ 유저가 빠른 스크롤을 할때 미리onBindViewHolder
를 미리 호출한 것을 보여주기 때문에 자연스러운 스크롤을 달성할 수 있다.- (1)
UI thread
에서View
의inflate
와bind
가 완료되면
(2) 순차적으로GPU Render Thread
에서 렌더링 작업을 하게 되는데,
(3) 이때UI Thread
는 유휴 상태가 된다.
→ 스크롤 할때 이 순서를 반복 - 하지만 스크롤할때 새로운
View
가 등장하기 위해UI Thread
는 다시infalte & bind
작업을 하게 되는데, 문제는 위 작업이Render Thread
에서 렌더링이 끝나고 UI가 사용자 눈에 표시 되려 하는 순간과 겹치게 된다.
→ 그래서UI Thread
의View
infate & bind
작업은 비용이 매우 크면서 동시에 렌더링 된 UI가 표시가 되기 때문에
→ 순간적으로 버벅거림이 생기게 된다. - 따라서
Prefetch 방식
을 통해 스크롤 할때inflate
가 필요한 경우,UI 스레드
가 유휴상태에 들어가는 동안 미리 추가적으로onBindViewHolder()
를 호출한다.
사용 조건
RecyclerView
버전이 업데이트 되면서setItemPrefetchEnabled()
가 기본적으로true
로 설정되어 있다. 그러나,ViewCache
메모리를 추가적으로 사용하게 된다.
→ 추가적인 비용 소요- 따라서
스크롤이 조금만 발생을 하도록 유도한 UX
,전체 아이템 갯수가 매우 적을 경우
→false
반대로많은 스크롤이 일어나는 일반적인 경우
→true
init
시에만 AsycLayoutInflate
사용, UI 스케줄러 제작
(안드로이드 개발자 Phil Olson의 방식 )
RecyclerView
에adapter
를set
하면서 동시에RecyclerView Adapter
를 사용하는 경우가 아닌,Adapter
에 뒤늦게 아이템이 insert 되는 경우,AsycLayoutInflate
를 사용해 미리onCreateViewHolder
에서 사용할View
를 만들어 두어onCreateViewHolder
의 부담을 더는 방법
예시
class SmoothListAdapter(val context: Context) :
ListAdapter<ListItem, SmoothListAdapter.ListItemViewHolder>(ListItemViewHolder.MyDiffCallback()) {
data class ListItem(val id: String, val text: String)
class ListItemViewHolder(view: View) : RecyclerView.ViewHolder(view) {
private val asyncLayoutInflater = AsyncLayoutInflater(context)
private val cachedViews = Stack<View>()
init {
//Create some views asynchronously and add them to our stack
for (i in 0..NUM_CACHED_VIEWS) {
asyncLayoutInflater.inflate(
R.layout.list_item,
null
) { view, layoutRes, viewGroup -> cachedViews.push(view) }
}
}
override fun onCreateViewHolder(
parent: ViewGroup,
viewType: Int
): ListItemViewHolder {
//Use the cached views if possible, otherwise if we ran out of cached views inflate a new one
val view = if (cachedViews.isEmpty()) {
LayoutInflater.from(context).inflate(R.layout.list_item, parent, false)
} else {
cachedViews.pop().also {
it.layoutParams = LinearLayout.LayoutParams(MATCH_PARENT, WRAP_CONTENT)
}
}
return ListItemViewHolder(view)
}
fun populateFrom(listItem: ListItem) {
//TODO: populate your view
}
override fun onBindViewHolder(viewHolder: ListItemViewHolder, position: Int) =
viewHolder.populateFrom(getItem(position))
class MyDiffCallback : DiffUtil.ItemCallback<ListItem>() {
override fun areItemsTheSame(firstItem: ListItem, secondItem: ListItem) =
firstItem.id == secondItem.id override fun areContentsTheSame(
firstItem: ListItem,
secondItem: ListItem
) = firstItem == secondItem
}
companion object {
const val NUM_CACHED_VIEWS = 5
}
}
}
- 만약 위 코드 처럼
ListAdapter
를 사용한다면 미리adapter
를 초기화 해두고submitList(colleaction<T>)
를 뒤늦게 사용하게 된다면 미리 만들어둔view
를stack
에 담아놨던걸 그대로 사용하면 된다. - 또한
UI 스케줄러
를 따로 만들어 사용 가능하다.
object UIJobScheduler {
private const val MAX_JOB_TIME_MS: Float = 4f private
var elapsed = 0L private
val jobQueue = ArrayDeque<() -> Unit>() private
val isOverMaxTime get () = elapsed > MAX_JOB_TIME_MS * 1_000_000 private
val handler = Handler()
fun submitJob(job: () -> Unit) {
jobQueue.add(job) if (jobQueue.size == 1) {
handler.post { processJobs() }
}
}
private fun processJobs() {
while (!jobQueue.isEmpty() && !isOverMaxTime) {
val start =
System.nanoTime() jobQueue . poll ().invoke() elapsed += System . nanoTime () - start
} if (jobQueue.isEmpty()) {
elapsed = 0
} else if (isOverMaxTime) {
onNextFrame { elapsed = 0 processJobs () }
}
}
private fun onNextFrame(callback: () -> Unit) =
Choreographer.getInstance().postFrameCallback { callback() }
}
- 위 코드는 최대 4ms를 사용한다.
MAX_JOB_TIME_MS
상수를 바꿔서 원하는 최대 ms를 설정할 수 있다.
AsyncLayoutInflater 를 사용해서 infalte, bind
(Dmitrii Kachan 의 방식)
AsyncLayoutInflater
를 사용해onCreateViewHolder
에서ViewHolder
를 생성하고,
생성 후onBindViewHolder
에서bind
까지 하는 방식
→ 기존에UI Thread
가 하던 일을background Thread
에 맡기는 형식
→background Thread
에서 레이아웃 계산부터 모두 비동기 방식으로 사용이 가능하다.
예시
open class AsyncCell(context: Context) : FrameLayout(context, null, 0, 0) {
init {
layoutParams = LayoutParams(MATCH_PARENT, WRAP_CONTENT)
}
open val layoutId = -1
// override with your layout Id
private var isInflated = false
private val bindingFunctions: MutableList<AsyncCell.() -> Unit> = mutableListOf()
fun inflate() {
AsyncLayoutInflater(context).inflate(layoutId, this) { view, _, _ ->
isInflated = true addView (createDataBindingView(view)) bindView ()
}
}
private fun bindView() {
with(bindingFunctions) {
forEach { it() }
clear()
}
}
fun bindWhenInflated(bindFunc: AsyncCell.() -> Unit) {
if (isInflated) {
bindFunc()
} else {
bindingFunctions.add(bindFunc)
}
}
// override for usage with dataBinding
open fun createDataBindingView(view: View): View? = view
}
- 위에
FrameLayout
를 더미로 만들고addView
를 해서 아래처럼 사용
class RecyclerViewAsyncAdapter(private val items: List<TestItem>) :
RecyclerView.Adapter<RecyclerView.ViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder =
SmallItemViewHolder(SmallItemCell(parent.context).apply { inflate() })
override fun getItemCount(): Int = items.size
override fun onBindViewHolder(
holder: RecyclerView.ViewHolder,
position: Int
) {
if (holder is SmallItemViewHolder) {
setUpSmallViewHolder(holder, position)
}
}
private fun setUpLargeViewHolder(holder: LargeItemViewHolder, position: Int) {
(holder.itemView as LargeItemCell).bindWhenInflated {
items[position].let { item ->
holder.itemView.binding?.item = item
}
}
}
private fun setUpSmallViewHolder(holder: SmallItemViewHolder, position: Int) {
(holder.itemView as SmallItemCell).bindWhenInflated {
items[position].let { item ->
holder.itemView.binding?.item = item
}
}
}
private inner class SmallItemViewHolder internal constructor(view: ViewGroup) : RecyclerView.ViewHolder(view)
private inner class SmallItemCell(context: Context) : AsyncCell(context) {
var binding: SmallItemCellBinding? = null override
val layoutId =
R.layout.small_item_cell override fun createDataBindingView(view: View): View? {
binding = SmallItemCellBinding.bind(view) return view.rootView
}
}
}
- 단점으로 복잡한
View
를 가진 아이템에서는 간혹 UI가 제대로 렌더링 못하는 경우가 있거나, 오히려 더 느려지는 문제점이 생기기도 한다.
→ 마찬가지로 상황에 맞게 사용해야 한다
ViewHolder
paramter로 넘어가는 View
를 inflate
해서 생성하지 않고 class로 만들기
onCreateViewHolder
에서 기본적으로XML
를LayoutInflate
로View
로inflate
해서 사용하는게 일반적인 경우- 대신
XML
를 사용하지 않고ItemView
자체를 class로 직접 만들어ViewHolder
paramter 로 넘기는 방법
→inflate
을 하지않고 바로View
를 넘기기 때문에ViewHolder
생성 비용을 아낄 수 있음
예시
class ProductCardView : MaterialCardView {
...
}
abstract class MyAdapter : RecyclerView.Adapter<MyAdapter.ViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
return RecyclerView.ViewHolder(
ProductCardView(parent.context)
)
}
...
}
단점
- 하지만
유지보수 측면
에서XML
에 비해 효율은 좋지 않음. - 성능을 중심으로 코딩 하는 상황이 아니라면 가급적 사용하지 않는 방법.
Compose LazyComposable 사용
- Compose를 통해 목록형 UI를 만드는 방법
늦은 초기화
를 통해 View가 사용자 눈에 보일 때 UI 렌더링 합니다.
→ 복잡한 Adpater 관리도 사라지며composable function
안에서 쉽게 List 관리가 가능하다.XML
과 혼용해 사용할 수 있기 때문에 List부분만LazyComposable
를 이용해 만드는 것도 하나의 방법
예시
LazyColumn {
// Add a single item
item { Text(text = "First item") }
// Add 5
items items (5) { index -> Text(text = "Item: $index") }
// Add another single
item item { Text(text = "Last item") }
}
반응형
'IT > Android' 카테고리의 다른 글
[Android/Refactoring] Memory Leak - 2. 자주 발생하는 안드로이드 메모리 누수 방지하기 (0) | 2023.07.25 |
---|---|
[Android/Refactoring] Memory Leak - 1. 안드로이드 앱에서의 메모리 누수 찾기 (1) | 2023.07.21 |
[Android] RecyclerView Deep Dive - 1. RecyclerView 정의와 동작원리 및 생명주기 (0) | 2023.07.05 |
[Android/FCM] (5) Android에서의 알림 수신 구현 (0) | 2023.03.11 |
[Android/FCM] (4) Android 설정 (0) | 2023.03.07 |