Konsep dan penerapan Jetpack Compose
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
RxPagingSourcesebagai gantinya. - Untuk menggunakan
ListenableFuturedari Guava, terapkanListenableFuturePagingSourcesebagai gantinya.
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.
Java (RxJava)
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 (Guava/LiveData)
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 mencakup kunci yang akan dimuat dan jumlah item yang akan dimuat.
Objek LoadResult berisi hasil operasi pemuatan.
LoadResult adalah class tertutup yang mengambil salah satu dari dua bentuk, 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.
load menggunakan dan memperbarui kunci.
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:
Java (RxJava)
return backend.searchUsers(searchTerm, nextPageNumber)
.subscribeOn(Schedulers.io())
.map(this::toLoadResult)
.onErrorReturn(LoadResult.Error::new);
Java (Guava/LiveData)
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 kesalahan Retrofit, lihat contoh dalam
referensi API PagingSource.
PagingSource mengumpulkan dan mengirimkan objek LoadResult.Error ke UI, sehingga
Anda dapat menindaklanjutinya. Untuk mengetahui 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
implementasi PagingSource Anda:
Java (RxJava)
// 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 (Guava/LiveData)
// 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 untuk menyediakan adaptor RecyclerView bagi item daftar
jenis User dan menggunakan UserViewHolder sebagai penampung tampilan:
Kotlin (Coroutine)
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 (RxJava)
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 (Guava/LiveData)
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 (Coroutine)
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 (RxJava)
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 (Guava/LiveData)
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
PagingDataAdapterke daftarRecyclerViewyang Anda inginkan untuk menampilkan data yang dibagi-bagi. - Amati aliran
PagingData, lalu teruskan setiap nilai yang dihasilkan ke metodesubmitData()adaptor Anda.
Kotlin (Coroutine)
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 (RxJava)
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 (Guava/LiveData)
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.