Paging 라이브러리는 대규모 데이터 세트에서 페이징된 데이터를 로드하고 표시하는 강력한 기능을 제공합니다. 이 가이드에서는 Paging 라이브러리를 사용하여 네트워크 데이터 소스에서 페이징된 데이터의 스트림을 설정하고 RecyclerView
에 표시하는 방법을 보여줍니다.
데이터 소스 정의
첫 번째 단계는 데이터 소스를 식별하기 위해 PagingSource
구현을 정의하는 것입니다. PagingSource
API 클래스에는 load()
메서드가 포함되어 있으며, 이 메서드는 상응하는 데이터 소스에서 페이징된 데이터를 검색하는 방법을 나타내기 위해 반드시 재정의해야 합니다.
PagingSource
클래스를 직접 사용하여 비동기 로드에 Kotlin 코루틴을 사용합니다. Paging 라이브러리는 다른 비동기 프레임워크를 지원하는 클래스도 제공합니다.
- RxJava를 사용하려면 대신
RxPagingSource
를 구현합니다. - Guava의
ListenableFuture
를 사용하려면 대신ListenableFuturePagingSource
를 구현합니다.
키 및 값 유형 선택
PagingSource<Key, Value>
에는 Key
와 Value
의 두 유형 매개변수가 있습니다. 키는 데이터를 로드하는 데 사용되는 식별자를 정의하며, 값은 데이터 자체의 유형입니다. 예를 들어 Int
페이지 번호를 Retrofit에 전달하여 네트워크에서 User
객체의 페이지를 로드하는 경우 Key
유형으로 Int
를, Value
유형으로 User
를 선택합니다.
PagingSource 정의
다음 예에서는 페이지 번호를 기준으로 항목 페이지를 로드하는 PagingSource
를 구현합니다. Key
유형은 Int
이고 Value
유형은 User
입니다.
Kotlin
class ExamplePagingSource( val backend: ExampleBackendService, val query: String ) : PagingSource<Int, User>() { override suspend fun load( params: LoadParams<Int> ): LoadResult<Int, User> { try { // Start refresh at page 1 if undefined. val nextPageNumber = params.key ?: 1 val response = backend.searchUsers(query, nextPageNumber) return LoadResult.Page( data = response.users, prevKey = null, // Only paging forward. nextKey = response.nextPageNumber ) } catch (e: Exception) { // Handle errors in this block and return LoadResult.Error for // expected errors (such as a network failure). } } override fun getRefreshKey(state: PagingState<Int, User>): Int? { // Try to find the page key of the closest page to anchorPosition from // either the prevKey or the nextKey; you need to handle nullability // here. // * prevKey == null -> anchorPage is the first page. // * nextKey == null -> anchorPage is the last page. // * both prevKey and nextKey are null -> anchorPage is the // initial page, so return null. return state.anchorPosition?.let { anchorPosition -> val anchorPage = state.closestPageToPosition(anchorPosition) anchorPage?.prevKey?.plus(1) ?: anchorPage?.nextKey?.minus(1) } } }
Java
class ExamplePagingSource extends RxPagingSource<Integer, User> { @NonNull private ExampleBackendService mBackend; @NonNull private String mQuery; ExamplePagingSource(@NonNull ExampleBackendService backend, @NonNull String query) { mBackend = backend; mQuery = query; } @NotNull @Override public Single<LoadResult<Integer, User>> loadSingle( @NotNull LoadParams<Integer> params) { // Start refresh at page 1 if undefined. Integer nextPageNumber = params.getKey(); if (nextPageNumber == null) { nextPageNumber = 1; } return mBackend.searchUsers(mQuery, nextPageNumber) .subscribeOn(Schedulers.io()) .map(this::toLoadResult) .onErrorReturn(LoadResult.Error::new); } private LoadResult<Integer, User> toLoadResult( @NonNull SearchUserResponse response) { return new LoadResult.Page<>( response.getUsers(), null, // Only paging forward. response.getNextPageNumber(), LoadResult.Page.COUNT_UNDEFINED, LoadResult.Page.COUNT_UNDEFINED); } @Nullable @Override public Integer getRefreshKey(@NotNull PagingState<Integer, User> state) { // Try to find the page key of the closest page to anchorPosition from // either the prevKey or the nextKey; you need to handle nullability // here. // * prevKey == null -> anchorPage is the first page. // * nextKey == null -> anchorPage is the last page. // * both prevKey and nextKey are null -> anchorPage is the // initial page, so return null. Integer anchorPosition = state.getAnchorPosition(); if (anchorPosition == null) { return null; } LoadResult.Page<Integer, User> anchorPage = state.closestPageToPosition(anchorPosition); if (anchorPage == null) { return null; } Integer prevKey = anchorPage.getPrevKey(); if (prevKey != null) { return prevKey + 1; } Integer nextKey = anchorPage.getNextKey(); if (nextKey != null) { return nextKey - 1; } return null; } }
Java
class ExamplePagingSource extends ListenableFuturePagingSource<Integer, User> { @NonNull private ExampleBackendService mBackend; @NonNull private String mQuery; @NonNull private Executor mBgExecutor; ExamplePagingSource( @NonNull ExampleBackendService backend, @NonNull String query, @NonNull Executor bgExecutor) { mBackend = backend; mQuery = query; mBgExecutor = bgExecutor; } @NotNull @Override public ListenableFuture<LoadResult<Integer, User>> loadFuture(@NotNull LoadParams<Integer> params) { // Start refresh at page 1 if undefined. Integer nextPageNumber = params.getKey(); if (nextPageNumber == null) { nextPageNumber = 1; } ListenableFuture<LoadResult<Integer, User>> pageFuture = Futures.transform(mBackend.searchUsers(mQuery, nextPageNumber), this::toLoadResult, mBgExecutor); ListenableFuture<LoadResult<Integer, User>> partialLoadResultFuture = Futures.catching(pageFuture, HttpException.class, LoadResult.Error::new, mBgExecutor); return Futures.catching(partialLoadResultFuture, IOException.class, LoadResult.Error::new, mBgExecutor); } private LoadResult<Integer, User> toLoadResult(@NonNull SearchUserResponse response) { return new LoadResult.Page<>(response.getUsers(), null, // Only paging forward. response.getNextPageNumber(), LoadResult.Page.COUNT_UNDEFINED, LoadResult.Page.COUNT_UNDEFINED); } @Nullable @Override public Integer getRefreshKey(@NotNull PagingState<Integer, User> state) { // Try to find the page key of the closest page to anchorPosition from // either the prevKey or the nextKey; you need to handle nullability // here. // * prevKey == null -> anchorPage is the first page. // * nextKey == null -> anchorPage is the last page. // * both prevKey and nextKey are null -> anchorPage is the // initial page, so return null. Integer anchorPosition = state.getAnchorPosition(); if (anchorPosition == null) { return null; } LoadResult.Page<Integer, User> anchorPage = state.closestPageToPosition(anchorPosition); if (anchorPage == null) { return null; } Integer prevKey = anchorPage.getPrevKey(); if (prevKey != null) { return prevKey + 1; } Integer nextKey = anchorPage.getNextKey(); if (nextKey != null) { return nextKey - 1; } return null; } }
일반적인 PagingSource
구현은 생성자에서 제공된 매개변수를 load()
메서드에 전달하여 쿼리에 적절한 데이터를 로드합니다. 위의 예에서 이러한 매개변수는 다음과 같습니다.
backend
: 데이터를 제공하는 백엔드 서비스의 인스턴스입니다.query
:backend
로 표시된 서비스에 전송할 검색어입니다.
LoadParams
객체에는 실행할 로드 작업에 관한 정보가 포함됩니다. 여기에는 로드할 키와 로드할 항목 수가 포함됩니다.
LoadResult
객체에는 로드 작업의 결과가 포함됩니다. LoadResult
는 load()
호출이 성공했는지 여부에 따라 두 가지 형식 중 하나를 취하는 봉인 클래스입니다.
- 로드에 성공하면
LoadResult.Page
객체를 반환합니다. - 로드에 실패하면
LoadResult.Error
객체를 반환합니다.
다음 그림은 이 예의 load()
함수가 각 로드의 키를 수신하고 후속 로드용 키를 제공하는 방법을 보여줍니다.
PagingSource
구현은 PagingState
객체를 매개변수로 사용하는 getRefreshKey()
메서드도 반드시 구현해야 합니다. 이 메서드는 데이터가 첫 로드 후 새로고침되거나 무효화되었을 때 키를 반환하여 load()
메서드로 전달합니다. Paging 라이브러리는 다음에 데이터를 새로고침할 때 자동으로 이 메서드를 호출합니다.
오류 처리
데이터 로드 요청은 특히 네트워크를 통해 로드하는 경우 여러 가지 이유로 실패할 수 있습니다. 로드하는 중에 load()
메서드에서 LoadResult.Error
객체를 반환하여 발생한 오류를 보고합니다.
예를 들어 load()
메서드에 다음을 추가하여 이전 예시의 ExamplePagingSource
에서 로드 오류를 포착하여 보고할 수 있습니다.
Kotlin
catch (e: IOException) { // IOException for network failures. return LoadResult.Error(e) } catch (e: HttpException) { // HttpException for any non-2xx HTTP status codes. return LoadResult.Error(e) }
Java
return backend.searchUsers(searchTerm, nextPageNumber) .subscribeOn(Schedulers.io()) .map(this::toLoadResult) .onErrorReturn(LoadResult.Error::new);
Java
ListenableFuture<LoadResult<Integer, User>> pageFuture = Futures.transform( backend.searchUsers(query, nextPageNumber), this::toLoadResult, bgExecutor); ListenableFuture<LoadResult<Integer, User>> partialLoadResultFuture = Futures.catching( pageFuture, HttpException.class, LoadResult.Error::new, bgExecutor); return Futures.catching(partialLoadResultFuture, IOException.class, LoadResult.Error::new, bgExecutor);
Retrofit 오류 처리에 관한 자세한 내용은 PagingSource
API 참조의 샘플을 참고하세요.
PagingSource
는 개발자가 조치를 취할 수 있도록 LoadResult.Error
객체를 수집하여 UI에 제공합니다. UI에서 로드 상태를 노출하는 방법에 관한 자세한 내용은 로드 상태 관리 및 표시를 참고하세요.
PagingData 스트림 설정
그런 다음 PagingSource
구현에서 페이징된 데이터의 스트림이 필요합니다.
ViewModel
에서 데이터 스트림을 설정하세요. Pager
클래스는 PagingSource
에서 PagingData
객체의 반응형 스트림을 노출하는 메서드를 제공합니다. Paging 라이브러리는 Flow
, LiveData
, RxJava의 Flowable
유형과 Observable
유형을 비롯한 여러 스트림 유형을 사용할 수 있도록 지원합니다.
Pager
인스턴스를 만들어 반응형 스트림을 설정할 때는 PagingConfig
구성 객체와 PagingSource
구현 인스턴스를 가져오는 방법을 Pager
에 지시하는 함수를 인스턴스에 제공해야 합니다.
Kotlin
val flow = Pager( // Configure how data is loaded by passing additional properties to // PagingConfig, such as prefetchDistance. PagingConfig(pageSize = 20) ) { ExamplePagingSource(backend, query) }.flow .cachedIn(viewModelScope)
Java
// CoroutineScope helper provided by the lifecycle-viewmodel-ktx artifact. CoroutineScope viewModelScope = ViewModelKt.getViewModelScope(viewModel); Pager<Integer, User> pager = Pager<>( new PagingConfig(/* pageSize = */ 20), () -> ExamplePagingSource(backend, query)); Flowable<PagingData<User>> flowable = PagingRx.getFlowable(pager); PagingRx.cachedIn(flowable, viewModelScope);
Java
// CoroutineScope helper provided by the lifecycle-viewmodel-ktx artifact. CoroutineScope viewModelScope = ViewModelKt.getViewModelScope(viewModel); Pager<Integer, User> pager = Pager<>( new PagingConfig(/* pageSize = */ 20), () -> ExamplePagingSource(backend, query)); PagingLiveData.cachedIn(PagingLiveData.getLiveData(pager), viewModelScope);
cachedIn()
연산자는 데이터 스트림을 공유 가능하게 하며 제공된 CoroutineScope
을 사용하여 로드된 데이터를 캐시합니다. 이 예에서는 수명 주기 lifecycle-viewmodel-ktx
아티팩트가 제공하는 viewModelScope
을 사용합니다.
Pager
객체는 PagingSource
객체에서 load()
메서드를 호출하여 LoadParams
객체를 제공하고 반환되는 LoadResult
객체를 수신합니다.
RecyclerView 어댑터 정의
데이터를 RecyclerView
목록에 수신하는 어댑터도 설정해야 합니다. Paging 라이브러리는 이러한 용도로 PagingDataAdapter
클래스를 제공합니다.
PagingDataAdapter
를 확장하는 클래스를 정의합니다. 아래 예에서 UserAdapter
는 PagingDataAdapter
를 확장하고 유형 User
의 목록 항목에 관해 UserViewHolder
를 뷰 홀더로 사용하여 RecyclerView
어댑터를 제공합니다.
Kotlin
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) // Note that item can be null. ViewHolder must support binding a // null item as a placeholder. holder.bind(item) } }
Java
class UserAdapter extends PagingDataAdapter<User, UserViewHolder> { UserAdapter(@NotNull DiffUtil.ItemCallback<User> diffCallback) { super(diffCallback); } @NonNull @Override public UserViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { return new UserViewHolder(parent); } @Override public void onBindViewHolder(@NonNull UserViewHolder holder, int position) { User item = getItem(position); // Note that item can be null. ViewHolder must support binding a // null item as a placeholder. holder.bind(item); } }
Java
class UserAdapter extends PagingDataAdapter<User, UserViewHolder> { UserAdapter(@NotNull DiffUtil.ItemCallback<User> diffCallback) { super(diffCallback); } @NonNull @Override public UserViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { return new UserViewHolder(parent); } @Override public void onBindViewHolder(@NonNull UserViewHolder holder, int position) { User item = getItem(position); // Note that item can be null. ViewHolder must support binding a // null item as a placeholder. holder.bind(item); } }
또한 어댑터는 onCreateViewHolder()
및 onBindViewHolder()
메서드를 정의하고 DiffUtil.ItemCallback
을 지정해야 합니다.
RecyclerView
목록 어댑터를 정의할 때의 일반적인 방식으로 동일하게 작동합니다.
Kotlin
object UserComparator : DiffUtil.ItemCallback<User>() { override fun areItemsTheSame(oldItem: User, newItem: User): Boolean { // Id is unique. return oldItem.id == newItem.id } override fun areContentsTheSame(oldItem: User, newItem: User): Boolean { return oldItem == newItem } }
Java
class UserComparator extends DiffUtil.ItemCallback<User> { @Override public boolean areItemsTheSame(@NonNull User oldItem, @NonNull User newItem) { // Id is unique. return oldItem.id.equals(newItem.id); } @Override public boolean areContentsTheSame(@NonNull User oldItem, @NonNull User newItem) { return oldItem.equals(newItem); } }
Java
class UserComparator extends DiffUtil.ItemCallback<User> { @Override public boolean areItemsTheSame(@NonNull User oldItem, @NonNull User newItem) { // Id is unique. return oldItem.id.equals(newItem.id); } @Override public boolean areContentsTheSame(@NonNull User oldItem, @NonNull User newItem) { return oldItem.equals(newItem); } }
UI에 페이징된 데이터 표시
지금까지 PagingSource
를 정의하고 앱에서 PagingData
스트림을 생성하는 방법을 만들었으며 PagingDataAdapter
를 정의했습니다. 이제 이러한 요소를 함께 연결하여 페이징된 데이터를 활동에 표시할 준비가 되었습니다.
활동의 onCreate
또는 프래그먼트의 onViewCreated
메서드에서 다음 단계를 진행하세요.
PagingDataAdapter
클래스의 인스턴스를 만듭니다.- 페이징된 데이터를 표시할
RecyclerView
목록에PagingDataAdapter
인스턴스를 전달합니다. PagingData
스트림을 확인하고 생성된 각 값을 어댑터의submitData()
메서드에 전달합니다.
Kotlin
val viewModel by viewModels<ExampleViewModel>() val pagingAdapter = UserAdapter(UserComparator) val recyclerView = findViewById<RecyclerView>(R.id.recycler_view) recyclerView.adapter = pagingAdapter // Activities can use lifecycleScope directly; fragments use // viewLifecycleOwner.lifecycleScope. lifecycleScope.launch { viewModel.flow.collectLatest { pagingData -> pagingAdapter.submitData(pagingData) } }
Java
ExampleViewModel viewModel = new ViewModelProvider(this) .get(ExampleViewModel.class); UserAdapter pagingAdapter = new UserAdapter(new UserComparator()); RecyclerView recyclerView = findViewById<RecyclerView>( R.id.recycler_view); recyclerView.adapter = pagingAdapter viewModel.flowable // Using AutoDispose to handle subscription lifecycle. // See: https://github.com/uber/AutoDispose. .to(autoDisposable(AndroidLifecycleScopeProvider.from(this))) .subscribe(pagingData -> pagingAdapter.submitData(lifecycle, pagingData));
Java
ExampleViewModel viewModel = new ViewModelProvider(this) .get(ExampleViewModel.class); UserAdapter pagingAdapter = new UserAdapter(new UserComparator()); RecyclerView recyclerView = findViewById<RecyclerView>( R.id.recycler_view); recyclerView.adapter = pagingAdapter // Activities can use getLifecycle() directly; fragments use // getViewLifecycleOwner().getLifecycle(). viewModel.liveData.observe(this, pagingData -> pagingAdapter.submitData(getLifecycle(), pagingData));
이제 RecyclerView
목록에 데이터 소스에서 페이징된 데이터가 표시되고 필요한 경우 자동으로 다른 페이지가 로드됩니다.
추가 리소스
Paging 라이브러리에 관한 자세한 내용은 다음과 같은 추가 리소스를 참고하세요.
Codelab
샘플
추천 서비스
- 참고: JavaScript가 사용 중지되어 있으면 링크 텍스트가 표시됩니다.
- 네트워크 및 데이터베이스의 페이지
- Paging 3으로 이전
- Paging 라이브러리 개요