Library Paging menyediakan kemampuan andal untuk memuat dan menampilkan
data yang di-page dari set data yang lebih besar. Panduan ini menunjukkan cara menggunakan Library Paging untuk menyiapkan aliran data yang di-page dari sumber data jaringan dan menampilkannya
di RecyclerView
.
Menentukan sumber data
Langkah pertama adalah menentukan
implementasi PagingSource
untuk mengidentifikasi sumber data. Class PagingSource
API mencakup
metode load()
,
yang Anda ganti untuk menunjukkan cara mengambil data yang dibagi-bagi dari
sumber data yang sesuai.
Gunakan class PagingSource
secara langsung untuk menggunakan coroutine Kotlin untuk pemuatan
asinkron. Library Paging juga menyediakan class untuk mendukung framework
asinkron lainnya:
- Untuk menggunakan RxJava, terapkan
RxPagingSource
. - Untuk menggunakan
ListenableFuture
dari Guava, terapkanListenableFuturePagingSource
.
Memilih jenis kunci dan nilai
PagingSource<Key, Value>
memiliki dua jenis parameter: Key
dan Value
. Kunci
menentukan ID yang digunakan untuk memuat data, dan nilainya adalah jenis
data itu sendiri. Misalnya, jika Anda memuat halaman objek User
dari jaringan
dengan meneruskan nomor halaman Int
ke
Retrofit,
pilih Int
sebagai jenis Key
dan User
sebagai jenis Value
.
Menentukan PagingSource
Contoh berikut menerapkan
PagingSource
yang memuat
halaman item menurut nomor halaman. Jenis Key
adalah Int
dan jenis Value
adalah
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; } }
Penerapan PagingSource
standar akan meneruskan parameter yang diberikan dalam
konstruktornya ke metode load()
untuk memuat data yang sesuai untuk kueri. Dalam
contoh di atas, parameter tersebut adalah:
backend
: instance layanan backend yang menyediakan data tersebutquery
: kueri penelusuran yang akan dikirim ke layanan yang ditunjukkan olehbackend
Objek LoadParams
berisi informasi tentang operasi pemuatan yang akan dilakukan. Ini
termasuk kunci dan jumlah item yang akan dimuat.
Objek LoadResult
berisi hasil operasi pemuatan. LoadResult
adalah class tertutup
yang mengambil salah satu dari dua metode, bergantung pada apakah panggilan load()
berhasil:
- Jika pemuatan berhasil, tampilkan objek
LoadResult.Page
. - Jika pemuatan gagal, tampilkan objek
LoadResult.Error
.
Gambar berikut menggambarkan cara fungsi load()
dalam contoh ini
menerima kunci untuk setiap pemuatan dan memberikan kunci untuk pemuatan berikutnya.
Implementasi PagingSource
juga harus menerapkan metode
getRefreshKey()
yang memerlukan objek
PagingState
sebagai
parameter. Metode ini menampilkan kunci untuk diteruskan ke metode load()
saat data
dimuat ulang atau dibatalkan validasinya setelah pemuatan awal. Library Paging memanggil
metode ini secara otomatis saat refresh data berikutnya.
Menangani error
Permintaan untuk memuat data dapat gagal karena sejumlah alasan, terutama saat memuat
melalui jaringan. Laporkan error yang terjadi selama pemuatan dengan menampilkan
objek LoadResult.Error
dari metode load()
.
Misalnya, Anda dapat menangkap dan melaporkan kesalahan pemuatan di ExamplePagingSource
dari contoh sebelumnya dengan menambahkan hal berikut ke metode load()
:
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);
Untuk informasi selengkapnya tentang penanganan error Retrofit, lihat contoh dalam
referensi API PagingSource
.
PagingSource
mengumpulkan dan mengirimkan objek LoadResult.Error
ke UI, sehingga
Anda dapat menindaklanjutinya. Untuk informasi selengkapnya tentang menampilkan status pemuatan
di UI, lihat Mengelola dan menampilkan status
pemuatan.
Menyiapkan aliran PagingData
Selanjutnya, Anda memerlukan aliran data yang dibagi-bagi dari implementasi PagingSource
.
Menyiapkan aliran data di ViewModel
Anda. Class
Pager
menyediakan metode yang
menampilkan aliran reaktif
objek PagingData
dari
PagingSource
. Library Paging mendukung penggunaan beberapa jenis aliran data,
termasuk Flow
, LiveData
, serta jenis Flowable
dan Observable
dari
RxJava.
Saat membuat instance Pager
untuk menyiapkan aliran reaktif, Anda harus
menyediakan instance dengan
objek konfigurasi PagingConfig
dan fungsi yang memberi tahu Pager
cara mendapatkan instance dari
penerapan PagingSource
Anda:
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);
Operator cachedIn()
membuat aliran data dapat dibagikan dan menyimpan data yang dimuat ke cache
dengan CoroutineScope
yang disediakan. Contoh ini menggunakan viewModelScope
yang disediakan oleh artefak lifecycle-viewmodel-ktx
siklus proses.
Objek Pager
memanggil metode load()
dari objek PagingSource
,
yang menyediakannya dengan
objek LoadParams
dan sebaliknya menerima
objek LoadResult
.
Menentukan adaptor RecyclerView
Anda juga perlu menyiapkan adaptor untuk menerima data ke dalam daftar RecyclerView
.
Library Paging menyediakan class PagingDataAdapter
untuk
tujuan ini.
Tentukan class yang memperluas PagingDataAdapter
. Dalam contoh tersebut,
UserAdapter
memperluas PagingDataAdapter
guna menyediakan adaptor RecyclerView
untuk item daftar jenis User
dan menggunakan UserViewHolder
sebagai penampung
tampilan:
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); } }
Adaptor Anda juga harus menentukan metode onCreateViewHolder()
dan
onBindViewHolder()
serta menentukan
DiffUtil.ItemCallback
.
Caranya sama seperti biasanya saat menentukan adaptor daftar
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); } }
Menampilkan data yang di-page dalam UI Anda
Setelah Anda menentukan PagingSource
, membuat cara bagi aplikasi untuk
menghasilkan aliran PagingData
, dan menentukan PagingDataAdapter
, Anda
siap menghubungkan elemen-elemen tersebut dan menampilkan data yang di-page dalam
aktivitas.
Lakukan langkah-langkah berikut dalam metode onCreate
aktivitas atau
onViewCreated
fragmen:
- Buat instance class
PagingDataAdapter
. - Teruskan instance
PagingDataAdapter
ke daftarRecyclerView
yang Anda inginkan untuk menampilkan data yang dibagi-bagi. - Amati aliran
PagingData
, lalu teruskan setiap nilai yang dihasilkan ke metodesubmitData()
adaptor Anda.
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));
Daftar RecyclerView
sekarang menampilkan data yang di-page dari sumber data dan
otomatis memuat halaman lain jika diperlukan.
Referensi lainnya
Untuk mempelajari library Paging lebih lanjut, lihat referensi tambahan berikut:
Codelab
Direkomendasikan untuk Anda
- Catatan: teks link ditampilkan saat JavaScript nonaktif
- Halaman dari jaringan dan database
- Bermigrasi ke Paging 3
- Ringkasan library paging