Memuat dan menampilkan data yang dibagi-bagi

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 halaman dari sumber data jaringan dan menampilkannya dalam daftar lambat.

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.

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 mengimplementasikan PagingSource yang memuat halaman item berdasarkan nomor halaman. Jenis Key adalah Int dan jenis Value adalah User.

class ExamplePagingSource(
    val backend: ExampleBackendService,
    val query: String
) : PagingSource<Int, User>() {
  override suspend fun load(
    params: LoadParams<Int>
  ): LoadResult<Int, User> {

    init {
        // the data source is expected to be immutable
        // invalidate PagingSource if data source
        // has updated
        backEnd.addDatabaseOnChangedListener {
            invalidate()
        }
    }

    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 = nextPageNumber + 1
      )
    } 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)
    }
  }
}

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 tersebut
  • query: kueri penelusuran yang akan dikirim ke layanan yang ditunjukkan oleh backend

Objek LoadParams berisi informasi tentang operasi pemuatan yang akan dilakukan. Ini termasuk kunci dan jumlah item yang akan dimuat. Untuk membedakan jenis muatan di load, periksa subclass LoadParams yang diteruskan, apakah LoadParams.Refresh, LoadParams.Append, atau LoadParams.Prepend.

Objek LoadResult berisi hasil operasi pemuatan. LoadResult adalah class tertutup yang mengambil salah satu dari tiga bentuk, bergantung pada apakah panggilan load berhasil:

  • Jika pemuatan berhasil, tampilkan objek LoadResult.Page. Teruskan null untuk nextKey atau prevKey jika tidak ada lagi data yang dapat dimuat ke arah tersebut.
  • Jika pemuatan gagal, tampilkan objek LoadResult.Error.
  • Jika PagingSource tidak lagi valid karena data pokok berubah, tampilkan objek LoadResult.Invalid.

Gambar berikut menggambarkan cara fungsi load dalam contoh ini menerima kunci untuk setiap pemuatan dan memberikan kunci untuk pemuatan berikutnya.

Pada setiap panggilan pemuatan, ExamplePagingSource mengambil kunci saat ini
    dan menampilkan kunci berikutnya yang akan dimuat.
Gambar 1. Diagram yang menunjukkan cara 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. Untuk penomoran halaman berbasis offset dengan kunci yang mewakili posisi item, Anda dapat menghitung kunci refresh yang relatif terhadap state.anchorPosition:

// Only for positional or offset PagingSources where Key is a 0-based item index
// rather than a page number.
override fun getRefreshKey(state: PagingState<Int, User>): Int? {
  // Center the refreshed load around the user's current viewport so the list
  // does not jump back to the top on invalidation.
  return state.anchorPosition?.let { anchorPosition ->
    (anchorPosition - state.config.initialLoadSize / 2).coerceAtLeast(0)
  }
}

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:

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)
}

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 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 mengekspos aliran data sebagai Flow.

Saat membuat instance Pager untuk menyiapkan streaming reaktif, Anda harus memberikan objek konfigurasi PagingConfig dan fungsi yang memberi tahu Pager cara mendapatkan instance implementasi PagingSource Anda. Anda juga dapat menentukan initialKey opsional, seperti 1 untuk nomor halaman berbasis 1 atau 0 untuk paging berbasis offset. Di PagingConfig, Anda dapat mengonfigurasi pageSize, initialLoadSize, prefetchDistance, dan enablePlaceholders. Properti prefetchDistance menentukan jumlah item dari salah satu tepi konten yang dimuat yang memicu pengambilan berikutnya.

class UserViewModel(
    private val backend: ExampleBackendService,
    private val query: String
) : ViewModel() {

    val userPagingFlow: Flow<PagingData<User>> = Pager(
        config = PagingConfig(
            pageSize = 20,
            // Load 3 pages initially so the user can scroll immediately without
            // hitting a loading state.
            initialLoadSize = 60,
            // Fetch the next page far enough ahead of the scroll position to
            // avoid visible pauses.
            prefetchDistance = 20,
            enablePlaceholders = true
        ),
        initialKey = 1,
        pagingSourceFactory = {
            ExamplePagingSource(backend, query)
        }
    )
    .flow
    .cachedIn(viewModelScope)
}

Operator cachedIn membuat aliran data dapat dibagikan dan menyimpan data yang dimuat ke cache dengan CoroutineScope yang disediakan. Tanpa cachedIn, PagingData tidak dapat diingat kembali. 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.

Mengumpulkan dan menampilkan data di UI Anda

Untuk menghubungkan streaming halaman ke UI, dapatkan flow dari ViewModel dan teruskan ke composable daftar.

@Composable
fun UserScreen(viewModel: UserViewModel = viewModel()) {
    val userFlow = viewModel.userPagingFlow
    UserList(flow = userFlow)
}

Gunakan collectAsLazyPagingItems untuk mengonversi flow PagingData menjadi LazyPagingItems. Kemudian, gunakan items API dalam LazyColumn untuk menata letak setiap item.

Pastikan untuk memberikan ID yang unik dan stabil untuk setiap item menggunakan itemKey. Contoh berikut menggunakan it.id (mereferensikan properti User.id) karena tetap stabil untuk instance User di seluruh update data.

@Composable
fun UserList(flow: Flow<PagingData<User>>) {
    val lazyPagingItems = flow.collectAsLazyPagingItems()
    LazyColumn {
        items(
            lazyPagingItems.itemCount,
            key = lazyPagingItems.itemKey { it.id }
        ) { index ->
            val user = lazyPagingItems[index]
            if (user != null) {
                UserRow(user)
            } else {
                UserPlaceholder()
            }
        }
    }
}

Library Paging menggunakan null untuk placeholder saat halaman dimuat, jadi jika Anda telah mengaktifkan placeholder, Anda harus menangani nilai null di blok konten.

Sekarang daftar menampilkan data halaman, dan library Paging memuat halaman tambahan saat pengguna men-scroll.

Referensi lainnya

Untuk mempelajari library Paging lebih lanjut, lihat referensi tambahan berikut:

Dokumentasi

Konten penayangan