Paging 라이브러리 개요
- Paging 라이브러리를 사용하면 로컬 저장소에서나 네트워크를 통해 대규모 데이터 세트의 페이지를 로드하고 표시할 수 있습니다.
- 구글 권장 Android 앱 아키텍처에 맞게 설계되었으며, 다른 Jetpack 구성요소와 원활하게 통합됩니다.
이점
- Paging된 데이터의 메모리 내 캐싱과 효율적인 시스템 리소스 활용이 가능합니다.
- 요청 중복 삭제 기능이 가능하여 네트워크 대역폭과 시스템 리소스를 효율적으로 사용할 수 있습니다.
- 로드된 데이터의 끝까지 스크롤할 경우 RecyclerView 어댑터가 자동으로 데이터를 요청합니다.
- 코루틴, LiveData, RxJava를 지원합니다.
- 새로고침 및 재시도 기능, 오류 처리를 지원합니다.
라이브러리 아키텍처
- Paging 라이브러리의 구성요소는 앱의 세 가지 레이어에서 작동합니다.
- data Layer
- ViewModel Layer
- UI Layer
Data Layer
- 저장소 레이어의 기본 페이징 라이브러리 구성요소는 PagingSource입니다.
- [PagingSource] 객체는 데이터 소스에서 데이터를 검색하는 방법을 정의합니다.
- 네트워크 소스 및 로컬 데이터베이스를 포함한 단일 소스에서 데이터를 로드할 수 있습니다.
- [RemoteMediator] 객체는 로컬 데이터베이스 캐시가 있는 네트워크 데이터 소스와 같은 계층화된 데이터 소스의 페이징을 처리합니다.
ViewModel Layer
- Pager 구성요소는 PagingSource 객체 및 PagingConfig 구성 객체를 바탕으로 반응형 스트림에 노출되는 PagingData 인스턴스 구성을 위한 API를 제공합니다.
- ViewModel 레이어를 UI에 연결하는 구성요소는 PagingData이며, 객체를 쿼리하여 결과를 저장합니다.
UI Layer
- UI 구성요소는 페이지로 나눈 데이터를 처리하는 RecyclerView 어댑터인 PagingDataAdapter가 됩니다.
- Compose를 사용하는 경우 collectAsLazyPagingItems()를 사용합니다.
PagingSource 정의하기
- Data Layer를 식별하기 위해서 PagingSource의 구현을 정의해야 합니다.
- PagingSourece<Key, Value>에는 Key와 Value의 두 유형 매개변수가 있습니다.
- 키는 데이터를 로드하는 데 사용되는 식별자를 정의하며, 값은 데이터 자체의 유형입니다.
PagingSource 정의
- 일반적으로 PagingSource 구현은 생성자에서 제공된 매개변수를 load() 메서드에 전달하여 쿼리에 적절한 데이터를 로드합니다.
- [service] : 데이터를 제공하는 백엔드 서비스의 인스턴스
- [query] : 인스턴스로 표시될 서비스에 전송할 검색어
- [LoadParams] : 실행할 로드 작업에 관한 정보를 포함
- [LoadResult] : 로드 작업의 결과를 포함하는 sealed class
- LoadResult.Page, LoadResult.Error 객체 반환
class ExamplePagingSource(
val service: ExamplePagingService,
val query: String,
) : PagingSource<Int, PagingUser>() {
// page 키를 찾는 함수
override fun getRefreshKey(
state: PagingState<Int, PagingUser>
): Int? {
val position = state.anchorPosition ?: return null
val anchorPage = state.closestPageToPosition(position)
val prevKey = anchorPage?.prevKey
val nextKey = anchorPage?.nextKey
// prevKey가 null이 아니라면 prevKey + 1 반환
// prevKey가 null이고, nextKey가 null이 아니라면 nextKey - 1 반환
// 모두 null이면 null 반환
// 이 경우 anchorPage는 page를 초기화합니다.
return prevKey?.plus(1) ?: nextKey?.minus(1)
}
override suspend fun load(
params: LoadParams<Int>
): LoadResult<Int, PagingUser> {
return try {
// key가 null이라면 1페이지부터 로드
val nextPageNumber = params.key ?: 1
val users = service.searchUser(query, nextPageNumber)
val prevPage = if (nextPageNumber > 1) nextPageNumber - 1 else null
val nextKey = if (users.isEmpty()) null else nextPageNumber + 1
LoadResult.Page(
data = users,
prevKey = prevPage,
nextKey =nextKey
)
} catch (e: Exception){
// error handle
LoadResult.Error(e)
}
}
}
load() 함수 동작
- load() 함수는 nextKey, prevKey를 중심으로 동작합니다.
- getRefreshKey() 메서드도 반드시 구현해야 하며, 데이터가 첫 로드 후 새로고침되거나 무효화되었을 때 키를 반환하여 load() 메서드로 전달합니다.
- Paging 라이브러리는 다음에 데이터를 새로고침할 때 자동으로 이 메서드를 호출합니다.
오류 처리
- 데이터 로드는 네트워크 오류 등 여러 가지 이유로 실패할 수 있습니다.
- 로드 중에 load() 메서드에서 LoadReuslt.Error를 반환하여 발생한 오류를 보고합니다.
PagingData 스트림
- PagingSource 구현에서 페이징된 데이터의 스트림이 필요합니다.
- ViewModel에서 데이터 스트림을 설정할 수 있습니다.
Pager
- Pager 클래스는 PagingSource에서 PagingData 객체의 반응형 스트림을 노출하는 메서드를 제공합니다.
- Flow, LiveData, RxJava의 Flowable
- PagingConfig 구성 객체와 PagingSource 구현 인스턴스를 가져오는 방법을 Pager에 제공해야 합니다.
class ExamplePagingViewModel(
service: ExamplePagingService,
) : ViewModel() {
private var query = ""
private val flow = Pager(
config = PagingConfig(
pageSize = 20
),
) {
ExamplePagingSource(
service = service,
query = query,
)
}.flow
.cachedIn(viewModelScope)
}
- cachedIn() 연산자를 통해 제공된 CoroutineScope에서 로드된 데이터를 캐시합니다.
- Pager 객체는 PagingSource 객체에서 load() 메서드를 호출하여 LoadParams 객체를 제공하고, 반환되는 LoadResult 객체를 수신합니다.
RecyclerView 어댑터
- 데이터를 RecyclerView 목록에 수신하는 어댑터를 설정해야 합니다.
- PagingDataAdapter를 사용할 수 있습니다.
PagingDataAdapter
- PagingDataAdapter를 확장하는 클래스를 정의합니다.
- 뷰 홀더를 사용하여 PagingUser 목록에 대한 RecyclerView 어댑터를 제공할 수 있습니다.
class ExamplePagingDataAdapter(
diffCallback: DiffUtil.ItemCallback<PagingUser>
): PagingDataAdapter<PagingUser,PagingUserViewHolder > (diffCallback){
override fun onBindViewHolder(holder: PagingUserViewHolder, position: Int) {
val item = getItem(position) ?: return
holder.bind(item)
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PagingUserViewHolder {
return PagingUserViewHolder(parent)
}
}
class PagingUserViewHolder(itemView: View) : ViewHolder(itemView){
fun bind( item: PagingUser){}
}
DiffUtil.ItemCallback
- DiffUtil.ItemCallback을 정의하여, 데이터 변화를 감지하고 자동으로 UI를 업데이트할 수 있습니다.
object PagingUserComparator: DiffUtil.ItemCallback<PagingUser>() {
override fun areItemsTheSame(oldItem: PagingUser, newItem: PagingUser): Boolean {
return oldItem.id == newItem.id
}
override fun areContentsTheSame(oldItem: PagingUser, newItem: PagingUser): Boolean {
return oldItem == newItem
}
}
UI에 페이징 데이터 표시
- onCreate 또는 onViewCreated에서 다음 단계를 진행하여 UI를 표시할 수 있습니다.
- PagingDataAdapter 클래스 인스턴스 생성
- RecyclerView에 Adapter 전달
- PagingData 스트림을 확인하고 생성된 각 값을 어댑터의 submitData() 메서드에 전달
class PagingFragment : Fragment() {
private val pagingService = ExamplePagingService()
private val viewModel = ExamplePagingViewModel(pagingService)
private val pagingDataAdapter = ExamplePagingDataAdapter(PagingUserComparator)
//recyclerView.adapter = pagingDataAdapter
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
lifecycleScope.launch {
viewModel.flow.collectLatest { pagingData ->
pagingDataAdapter.submitData(pagingData)
}
}
}
}