# Paging3란?
페이징 라이브러리는 로컬 데이터베이스 또는 네트워크(Remote)의 데이터를 페이지 단위로 UI에 쉽게 표현할 수 있도록 도와주는 라이브러리다.
라이브러리를 사용하지 않고 기존에 ScrollListener을 사용해서 무한스크롤을 구현하기 위해서는 RecyclerView와 같은 리스트 UI가 상단 또는 하단에 도달했는지 판단하는 코드를 작성하고, 다음 페이지를 로드(or Refresh)하는 코드를 또 작성해야만 했다.
네트워크오류, 스크롤 감지 이상과 같은예외 처리 코드도 상당했다
페이징은 데이터를 가져올 때 한 번에 모든 데이터를 가져오는 것이 아니라, 일정한 덩어리(페이지)로 나눠서 가져온다
예를 들어, 구글에서 어떤 키워드로 검색하게 되면 결과의 모든 데이터를 한 번에 가져오는 것이 아니라 페이지로 나누어 데이터를 가져오게 된다. 이러한 페이징 방식을 사용하면 앱에서 네트워크 대역폭과 시스템 리소스를 더 효율적으로 사용하기에 성능, 메모리, 비용 측면에서 굉장히 효율적이다
# Paging3 장점
- 페이징된 데이터의 메모리 내 캐싱, 요청중복제거 기능 제공 → 시스템 리소스 효율적으로 사용
- 스크롤할때 RecyclerView 어댑터가 자동으로 데이터를 요청
- Kotlin 코루틴 및 Flow뿐만 아니라 LiveData 및 RxJava를 최고 수준으로 지원
- 새로고침, 재시도 기능, 오류 처리 기본 지원
# Components
Paging3는 프로젝트의 적절한 관심사 분리를 요구하며 안드로이드 권장 아키텍처에 통합되도록 만들어졌다.
<공식문서 - Paging 라이브러리의 구성요소는 권장 Android 앱 아키텍처에 맞게 설계되었으며 다른 Jetpack 구성요소와 원활하게 통합되고 최고 수준으로 Kotlin을 지원합니다.>
Repository, ViewModel, UI 3가지 레이어에서 작동한다
- Repository Layer
PagingSource
데이터 소스와 이 소스에서 데이터를 검색하는 방법을 정의한다.
네트워크 소스 및 로컬 데이터베이스를 포함한 단일 소스에서 데이터를 로드할 수 있다
<로컬 데이터베이스 또는 네트워크로 데이터를 불러오는 것을 담당하는 추상 클래스이다. 데이터 소스를 정의하고 데이터를 가져오는 방법을 정의한다.>
RemoteMediator
RemoteMediator 객체는 로컬 데이터베이스 캐시가 있는 네트워크 데이터 소스와 같은 계층화된 데이터 소스의 페이징을 처리한다
<네트워크(Remote)에서 불러온 데이터를 로컬 데이터베이스에 캐시(Cache)하여 불러오는 것을 담당한다.
오프라인 상태에서도 캐시된 데이터를 불러옴으로 유저 경험을 향상시켜줄 수 있다.>
- ViewModel Layer
Pager
Pager 구성요소는 PagingSource 객체 및 PagingConfig 구성 객체를 바탕으로 반응형 스트림에 노출되는 PagingData 인스턴스를 구성하기 위한 공개 API를 제공한다
<Repository Layer에서 구현된 PagingSource과 함께 PagingData 인스턴스를 구성하는 반응형 스트림을 생성한다.
PagingSource에서 데이터를 로드하는 방법, 옵션을 정의한 PagingConfig 클래스와 함께 사용된다.>
PagingData
PagingData 객체는 페이지로 나눈 데이터의 스냅샷을 보유하는 컨테이너이다
PagingSource 객체를 쿼리하여 결과를 저장한다
<페이징된 데이터의 Container 역할을 한다. 데이터가 새로고침될 때마다 이에 상응하는 PagingData가 별도로 생성된다.>
- UI Layer
PagingDataAdapter
페이지로 나눈 데이터를 처리하는 RecyclerView 어댑터이다 -> 포함된 AsyncPagingDataDiffer 구성요소를 사용하여 고유한 맞춤 어댑터를 빌드할 수 있다
<PagingData를 RecyclerView에 바인딩하기 위해 사용된다.
데이터를 어느 시점에서 더 받아올 것인가 등 UI와 관련된 대부분의 일을 책임진다.>
# 코드
페이지 번호를 기준으로 항목 페이지를 로드하는 pagingSource를 구현한다
PagingSource 정의
class ExamplePagingSource(
val backend: ExampleBackendService,
val query: String
) : PagingSource<Int, User>() {
override suspend fun load(
params: LoadParams<Int>
): LoadResult<Int, User> {
try {
// 정의되지 않은 경우, 1페이지에서 새로고침 시작
val nextPageNumber = params.key ?: 1
val response = backend.searchUsers(query, nextPageNumber)
return LoadResult.Page(
data = response.users,
prevKey = null, //if (page == 0) null else page - 1
nextKey = response.nextPageNumber
)
} catch (e: Exception) {
LoadResult.Error(e)
}
}
override fun getRefreshKey(state: PagingState<Int, User>): Int? {
// 위치를 앵커 위치에서 가장 가까운 페이지의 페이지 키를 찾도록 시도합니다
// prevKey 또는 nextKey. nullability를 처리해야 합니다
// prevKey == null -> anchorPage가 첫 페이지입니다.
// * nextKey == null -> anchorPage is the last page.
// * nextKey == null -> anchorPage가 마지막 페이지입니다.
// * prevKey와 nextKey 모두 null입니다 -> 앵커 페이지는 초기 페이지이므로 null을 반환합니다.
return state.anchorPosition?.let { anchorPosition ->
val anchorPage = state.closestPageToPosition(anchorPosition)
anchorPage?.prevKey?.plus(1) ?: anchorPage?.nextKey?.minus(1)
}
}
}
먼저 데이터소스를 식벼라기위해 pagingSource를 정의한다
pagingSource API 클래스에는 load() 메서드가 포함되어있으며, 이 메서드는 상응하는 데이터 소스에서 페이징된 데이터를 검색하는 방법을 나타내기 위해 반드시 재정의해야한다
PagingSource 클래스를 직접 사용하여 비동기 로드에 Kotlin 코루틴을 사용한다
PagingSource<Key, Value>에는 Key와 Value의 두 유형 매개변수가 있는데,
키는 데이터를 로드하는 데 사용되는 식별자를 정의하며, 값은 데이터 자체의 유형이다
( 예를 들어 Int 페이지 번호를 Retrofit에 전달하여 네트워크에서 User 객체의 페이지를 로드하는 경우,
Key 유형으로 Int를, Value 유형으로 User를 선택한다)
일반적인 PagingSource 구현은, 생성자에서 제공된 매개변수를 load() 메서드에 전달하여 쿼리에 적절한 데이터를 로드한다
- backend: 데이터를 제공하는 백엔드 서비스의 인스턴스
- query: backend로 표시된 서비스에 전송할 검색어
LoadParams 객체
- 실행할 로드 작업에 관한 정보가 포함 (로드할 키, 로드할 항목 수가 포함)
LoadResult 객체
- 로드 작업의 결과가 포함
- load()호출이 성공했는지 여부에 따라 LoadResult.Page(로드성공시), LoadResult.Error(로드실패시) 객체 반환
로드 성공시에 API로 response.users라는 데이터를 가져와서 로드하는데,
params.key값이 정의되지 않은경우 새로고침시에는 1페이지부터 데이터를 로드하며,
이전페이지키는 null값을 반환하고, 다음페이지키는 response에서 받아온 nextPageNumber를 반환하는 코드이다
getRefreshKey() 메서드는 PagingState 객체를 매개변수로 사용하는 메서드인데, 반드시 구현해야하는 메서드이다.
이는 데이터가 첫 로드 후 새로고침되거나 무효화되었을 때, 키를 반환하여 load() 메서드로 전달한다
Paging 라이브러리는 데이터를 새로고침할 때 자동으로 이 메서드를 호출한다
데이터 스트림 설정
val flow = Pager(
// 추가 속성을 전달하여 데이터를 로드하는 방법 구성
PagingConfig(pageSize = 20)
) {
ExamplePagingSource(backend, query)
}.flow
.cachedIn(viewModelScope)
Pager 클래스는 PagingSource에서 PagingData 객체의 반응형 스트림을 노출하는 메서드를 제공한다
(Paging 라이브러리는 Flow, LiveData, RxJava의 Flowable 유형과 Observable 유형을 비롯한 여러 스트림 유형을 사용할 수 있도록 지원)
Pager 인스턴스를 만들어 반응형 스트림을 설정할 때는 PagingConfig 구성 객체와 PagingSource 구현 인스턴스를 가져오는 방법을 Pager에 지시하는 함수를 인스턴스에 제공해야한다
cachedIn() 연산자는 데이터 스트림을 공유 가능하게 하며 제공된 CoroutineScope을 사용하여 로드된 데이터를 캐시한다
Pager 객체는 PagingSource 객체에서 load() 메서드를 호출하여 LoadParams 객체를 제공하고, 반환되는 LoadResult 객체를 수신한다
RecyclerView 어댑터 정의
class UserAdapter(diffCallback: DiffUtil.ItemCallback<User>) :
PagingDataAdapter<User, UserViewHolder>(diffCallback) {
override fun onCreateViewHolder(
parent: ViewGroup,
viewType: Int
): UserViewHolder {
return UserViewHolder(parent)
}
override fun onBindViewHolder(holder: UserViewHolder, position: Int) {
val item = getItem(position)
// 항목이 null일 수 있습니다. ViewHolder는 바인딩을 지원해야 합니다
// 자리 표시자로서 null 항목입니다.
holder.bind(item)
}
}
object UserComparator : DiffUtil.ItemCallback<User>() {
override fun areItemsTheSame(oldItem: User, newItem: User): Boolean {
return oldItem.id == newItem.id
}
override fun areContentsTheSame(oldItem: User, newItem: User): Boolean {
return oldItem == newItem
}
}
데이터를 RecyclerView 목록에 수신하는 어댑터도 설정해야한다.
Paging 라이브러리는 이러한 용도로 PagingDataAdapter 클래스를 제공한다
또한 어댑터는 onCreateViewHolder() 및 onBindViewHolder() 메서드를 정의하고 DiffUtil.ItemCallback을 지정해야 한다
UI에 페이징된 데이터 표시
val viewModel by viewModels<ExampleViewModel>()
val pagingAdapter = UserAdapter(UserComparator)
val recyclerView = findViewById<RecyclerView>(R.id.recycler_view)
recyclerView.adapter = pagingAdapter
lifecycleScope.launch {
viewModel.flow.collectLatest { pagingData ->
pagingAdapter.submitData(pagingData)
}
}
지금까지 PagingSource를 정의하고 앱에서 PagingData 스트림을 생성하는 방법을 만들었으며, PagingDataAdapter를 정의했다.
이제 View딴에서 페이징된 데이터를 보여지게 해야하기 때문에
- 페이징된 데이터를 표시할 RecyclerView 목록에 PagingDataAdapter 인스턴스를 전달한다
- PagingData 스트림을 확인하고, 생성된 각 값을 어댑터의 submitData() 메서드에 전달한다.
(주의: submitData() 메서드는 PagingSource가 무효화되거나 어댑터의 새로고침 메서드가 호출될 때까지 중단되며 반환하지 않습니다. 즉 submitData() 호출 이후의 코드가 의도한 것보다 훨씬 늦게 실행될 수 있습니다.)
이렇게하면 RecyclerView목록에 데이터 소스에서 페이징된 데이터가 표시되고, 필요한 경우 자동으로 다음 페이지가 로드된다
#참고자료
https://developer.android.com/topic/libraries/architecture/paging/v3-overview?hl=ko
페이징 라이브러리 개요 | Android Developers
이 페이지는 Cloud Translation API를 통해 번역되었습니다. 컬렉션을 사용해 정리하기 내 환경설정을 기준으로 콘텐츠를 저장하고 분류하세요. 페이징 라이브러리 개요 Android Jetpack의 구성요소 Paging
developer.android.com
https://developer.android.com/topic/libraries/architecture/paging/v3-paged-data?hl=ko
페이징 데이터 로드 및 표시 | Android Developers
이 페이지는 Cloud Translation API를 통해 번역되었습니다. 페이징 데이터 로드 및 표시 컬렉션을 사용해 정리하기 내 환경설정을 기준으로 콘텐츠를 저장하고 분류하세요. Paging 라이브러리는 대규
developer.android.com
https://developer.android.com/topic/libraries/architecture/paging/v3-network-db?hl=ko
네트워크 및 데이터베이스의 페이지 | Android Developers
이 페이지는 Cloud Translation API를 통해 번역되었습니다. 네트워크 및 데이터베이스의 페이지 컬렉션을 사용해 정리하기 내 환경설정을 기준으로 콘텐츠를 저장하고 분류하세요. 네트워크 연결이
developer.android.com
https://developer.android.com/topic/libraries/architecture/paging/v3-transform?hl=ko
데이터 스트림 변환 | Android Developers
이 페이지는 Cloud Translation API를 통해 번역되었습니다. 데이터 스트림 변환 컬렉션을 사용해 정리하기 내 환경설정을 기준으로 콘텐츠를 저장하고 분류하세요. 페이징된 데이터로 작업할 때는
developer.android.com
https://velog.io/@godmin66/Paging-3-%EC%A0%81%EC%9A%A9%ED%95%98%EA%B8%B0
Paging 3 적용하기
Paging 3 라이브러리를 사용하면 로컬 저장소에서나 네트워크를 통해 대규모 데이터 세트의 데이터 페이지를 로드 하고 표시할 수 있습니다.이 방식을 사용하면 앱에서 네트워크 대역폭과 시스템
velog.io
'개발 노트 > Kotlin' 카테고리의 다른 글
[Android/Kotlin] Bottom Navigation View 구현 (0) | 2024.05.28 |
---|---|
[Android/Kotlin] KakaoMap API Android v2 사용하기 (1) | 2024.05.27 |
[Android/Kotlin] collect과 collectLatest의 차이점? [Flow] (0) | 2024.05.27 |
[Android/Kotlin] Flow 란? (StateFlow, SharedFlow) (1) | 2024.05.26 |
[Android/Kotlin] Hilt란? (0) | 2024.05.24 |