(Android/안드로이드) ViewPager2 사용할 때 반드시 주의해야 할 점 정리
개요
- ViewPager2는 내부적으로 RecyclerView 기반으로 동작합니다.
- 기존 ViewPager보다 유연하지만, 그만큼 Fragment 생명주기 / Adapter 갱신 / NestedScroll / 성능 이슈가 자주 발생합니다.
1. 구조
-
ViewPager2 └── RecyclerView └── FragmentViewHolder └── Fragment - Fragment가 “고정 View”가 아니라
- RecyclerView의 아이템처럼 생성/파괴됨
- 👉 그래서 기존 ViewPager와 다르게 ID 관리, lifecycle, notify 방식이 매우 중요합니다.
2. FragmentStateAdapter 사용 시 주의점
- notifyDataSetChanged()만 호출하면 화면이 안 바뀌는 문제
- 기본적으로 FragmentStateAdapter는 아이템 ID 기반 캐싱을 합니다.
// 화면이 안바뀔 수 있음
adapter.list = newList
adapter.notifyDataSetChanged()
- ✅ 해결: getItemId + containsItem override
class MyAdapter(
fragment: Fragment,
private var items: List<Item>
) : FragmentStateAdapter(fragment) {
override fun getItemCount() = items.size
override fun createFragment(position: Int): Fragment {
return MyFragment.newInstance(items[position])
}
override fun getItemId(position: Int): Long {
return items[position].id
}
override fun containsItem(itemId: Long): Boolean {
return items.any { it.id == itemId }
}
}
3. Fragment Lifecycle 오해
- ViewPager2
- 현재 페이지 → RESUMED
- 좌우 페이지 → STARTED
- 그 외 → CREATED
- 즉, 모든 Fragment가 RESUMED가 아님.
-
// 흔한 실수 override fun onResume() { super.onResume() loadData() // ❌ 여러 번 호출될 수 있음 } // 권장 패턴 override fun onViewCreated(...) { viewLifecycleOwner.lifecycleScope.launch { repeatOnLifecycle(Lifecycle.State.RESUMED) { // 화면에 보일 때만 실행 } } }
4. offscreenPageLimit 남용 금지
viewPager.offscreenPageLimit = 5 // ❌
- Fragment를 미리 다 생성
- 메모리 사용량 급증
- 저사양 단말에서 OOM/GC 지연
- 권장 : 기본값 유지 or 정말 필요한 1~2 정도
5. NestedScroll 충돌 (자주 발생)
- 구조
ViewPager2
└── RecyclerView
└── Fragment
└── RecyclerView
- 해결
recyclerView.isNestedScrollingEnabled = false
viewPager2.getChildAt(0).overScrollMode = RecyclerView.OVER_SCROLL_NEVER
- 서버에서 “없을 수 있는 값”은 nullable
- 앱이 꼭 필요한 값은 “DTO 단계에서 기본값” 부여 후 Domain으로 변환
6. TabLayoutMediator 사용 시 메모리 누수
TabLayoutMediator(tabLayout, viewPager2) { tab, position ->
tab.text = titles[position]
}.attach()
- Fragment onDestroyView 반드시 해제
private var mediator: TabLayoutMediator? = null
override fun onDestroyView() {
// detach 안 하면 View 참조 유지 → 누수 가능
mediator?.detach()
mediator = null
super.onDestroyView()
}