Dasar-Dasar Paging Android

1. Pengantar

Yang akan Anda pelajari

  • Komponen utama library paging.
  • Cara menambahkan library paging ke project Anda.

Yang akan Anda bangun

Dalam codelab ini, Anda akan memulai dengan aplikasi contoh yang sudah menampilkan daftar artikel. Daftar ini statis, memiliki 500 artikel, dan semuanya disimpan di memori ponsel:

7d256d9c74e3b3f5.png

Selama Anda mengikuti codelab, Anda akan:

  • ...diperkenalkan dengan konsep penomoran halaman.
  • ...diperkenalkan dengan komponen inti Library Paging.
  • ...menunjukkan cara menerapkan penomoran halaman dengan library Paging.

Setelah selesai, Anda akan memiliki sebuah aplikasi:

  • ...yang berhasil menerapkan penomoran halaman.
  • ...yang berkomunikasi secara efektif dengan pengguna jika jumlah data yang diambil lebih banyak.

Berikut pratinjau singkat UI yang akan kita kerjakan:

6277154193f7580.gif

Yang akan Anda butuhkan

Opsional

2. Menyiapkan Lingkungan Anda

Pada langkah ini, Anda akan mendownload kode untuk seluruh codelab kemudian menjalankan aplikasi contoh sederhana.

Untuk memulainya secepat mungkin, kami telah menyiapkan project awal untuk Anda kembangkan.

Jika sudah menginstal git, Anda cukup menjalankan perintah di bawah. Untuk memeriksa apakah git sudah diinstal, ketik git --version di terminal atau command line dan pastikan git dijalankan dengan benar.

 git clone https://github.com/googlecodelabs/android-paging

Jika belum memiliki git, Anda dapat mengklik tombol berikut untuk mendownload semua kode untuk codelab ini:

Kode disusun dalam dua folder, yaitu basic dan advanced. Untuk codelab ini, kita hanya akan berfokus pada folder basic.

Di folder basic, terdapat juga dua folder lainnya: start dan end. Kita akan mulai mengerjakan kode di folder start dan di akhir codelab, kode di folder start harus sama dengan yang ada di folder end.

  1. Buka project di direktori basic/start di Android Studio.
  2. Jalankan app yang menjalankan konfigurasi di perangkat atau emulator.

89af884fa2d4e709.png

Kita akan melihat daftar artikel. Scroll hingga ke bagian akhir untuk memverifikasi bahwa daftar bersifat statis—dengan kata lain, tidak akan ada lagi data yang diambil jika kita telah mencapai akhir daftar. Scroll kembali ke atas untuk memverifikasi bahwa kita masih memiliki semua item.

3. Pengantar penomoran halaman

Salah satu cara paling umum untuk menampilkan informasi kepada pengguna adalah dengan membuat daftar. Namun, terkadang daftar ini hanya menawarkan jendela kecil untuk semua konten yang tersedia bagi pengguna. Saat pengguna men-scroll informasi yang tersedia, sering kali dianggap bahwa data lainnya juga akan diambil untuk melengkapi informasi yang telah dilihat. Setiap kali data diambil, aktivitas tersebut harus efisien dan lancar sehingga pemuatan inkremental tidak menurunkan pengalaman pengguna. Pemuatan inkremental juga menawarkan manfaat performa karena aplikasi tidak perlu menyimpan data dalam jumlah besar di memori sekaligus.

Proses pengambilan informasi secara bertahap ini disebut penomoran halaman, dengan setiap halaman sesuai dengan bagian data yang akan diambil. Untuk meminta halaman, sumber data yang di-page sering kali memerlukan kueri yang menentukan informasi yang diperlukan. Bagian lainnya dalam codelab ini akan memperkenalkan library Paging, dan menunjukkan cara menggunakan codelab ini untuk membantu Anda menerapkan penomoran halaman di aplikasi dengan cepat dan efisien.

Komponen inti Library Paging

Komponen inti library Paging adalah sebagai berikut:

  • PagingSource - class dasar untuk memuat potongan data untuk kueri halaman tertentu. Ini adalah bagian dari lapisan data, dan biasanya diekspos dari class DataSource, lalu oleh Repository untuk digunakan di ViewModel.
  • PagingConfig - class yang menentukan parameter yang menentukan perilaku paging. Ini termasuk ukuran halaman, apakah placeholder diaktifkan atau tidak, dan sebagainya.
  • Pager - class yang bertanggung jawab untuk menghasilkan aliran data PagingData. PagingSource memegang kendali untuk melakukan ini dan harus dibuat di ViewModel.
  • PagingData - penampung untuk data yang telah dipaginasi. Setiap pemuatan ulang data akan memiliki emisi PagingData terkait yang didukung oleh PagingSource-nya sendiri.
  • PagingDataAdapter - subclass RecyclerView.Adapter yang menyajikan PagingData di RecyclerView. PagingDataAdapter dapat dihubungkan ke Flow Kotlin, LiveData, Flowable RxJava, Observable RxJava, atau bahkan daftar statis menggunakan metode factory. PagingDataAdapter akan memproses peristiwa pemuatan PagingData internal dan memperbarui UI secara efisien saat halaman dimuat.

566d0f6506f39480.jpeg

Di bagian berikut, Anda akan menerapkan contoh dari setiap komponen yang telah dijelaskan di atas.

4. Ringkasan project

Aplikasi dalam bentuknya saat ini menampilkan daftar artikel statis. Setiap artikel memiliki judul, deskripsi, dan tanggal pembuatannya. Daftar statis berfungsi dengan baik untuk beberapa item, tetapi skalanya tidak bertambah dengan baik karena set data menjadi lebih besar. Kita akan memperbaikinya dengan menerapkan penomoran halaman menggunakan library Paging, tetapi mari kita bahas komponen yang sudah ada di aplikasi terlebih dahulu.

Aplikasi mengikuti arsitektur yang direkomendasikan di panduan arsitektur aplikasi. Anda akan menemukan item berikut di setiap paket:

Lapisan data:

  • ArticleRepository: Bertanggung jawab untuk menyediakan daftar artikel dan menyimpannya di memori.
  • Article: Class yang mewakili model data, representasi dari informasi yang diambil dari lapisan data.

Lapisan UI:

  • Activity, RecyclerView.Adapter, dan RecyclerView.ViewHolder: Class yang bertanggung jawab untuk menampilkan daftar di UI.
  • ViewModel: Holder status yang bertanggung jawab untuk membuat status yang perlu ditampilkan UI.

Repositori mengekspos semua artikelnya di Flow dengan kolom articleStream. Kemudian dibaca oleh ArticleViewModel di lapisan UI yang kemudian menyiapkannya untuk digunakan oleh UI di ArticleActivity dengan kolom state, StateFlow.

Dengan mengekspos artikel sebagai Flow dari repositori, repositori akan bisa memperbarui artikel yang ditampilkan karena artikel tersebut terus berubah. Misalnya, jika judul artikel berubah, perubahan tersebut dapat dengan mudah disampaikan kepada kolektor articleStream. Penggunaan StateFlow untuk status UI di ViewModel memastikan bahwa meskipun kita berhenti mengumpulkan status UI—misalnya, saat Activity dibuat ulang selama perubahan konfigurasi—kita dapat langsung mengambil dari tempat terakhir jika kita ingin mulai mengumpulkannya lagi.

Seperti yang sudah disebutkan sebelumnya, articleStream saat ini dalam repositori hanya menyajikan berita untuk hari ini. Meskipun mungkin cukup untuk beberapa pengguna, pengguna lainnya mungkin ingin melihat artikel lama ketika mereka men-scroll semua artikel yang tersedia untuk hari ini. Ekspektasi ini menjadikan tampilan artikel sebagai opsi yang tepat untuk penomoran halaman. Alasan lain kita harus menjelajahi halaman melalui artikel yang meliputi:

  • ViewModel menyimpan semua item yang dimuat dalam memori di items StateFlow. Ini menjadi masalah utama jika ukuran set data menjadi sangat besar karena dapat memengaruhi performa.
  • Memperbarui satu atau beberapa artikel di daftar saat telah berubah akan menjadi lebih mahal jika daftar artikel makin besar.

Library Paging membantu menyelesaikan semua masalah ini sembari menyediakan API yang konsisten untuk mengambil data secara bertahap (penomoran halaman) di aplikasi Anda.

5. Menentukan sumber data

Saat menerapkan penomoran halaman, kita ingin memastikan kondisi berikut terpenuhi:

  • Menangani permintaan data dengan benar dari UI, memastikan beberapa permintaan tidak terpicu bersamaan untuk kueri yang sama.
  • Mempertahankan jumlah data yang diambil dan dapat dikelola di memori.
  • Memicu permintaan untuk mengambil lebih banyak data guna melengkapi data yang telah kita ambil.

Kita dapat melakukan semua itu dengan PagingSource. PagingSource menentukan sumber data dengan menentukan cara mengambil data dalam potongan inkremental. Kemudian objek PagingData menarik data dari PagingSource sebagai respons terhadap petunjuk pemuatan yang dibuat saat pengguna men-scroll di RecyclerView.

PagingSource kita akan memuat artikel. Di data/Article.kt, Anda akan menemukan model yang ditetapkan sebagai berikut:

data class Article(
    val id: Int,
    val title: String,
    val description: String,
    val created: LocalDateTime,
)

Untuk membangun PagingSource, Anda harus menentukan hal-hal berikut:

  • Jenis kunci paging - Definisi jenis kueri halaman yang kita gunakan untuk meminta lebih banyak data. Dalam kasus ini, kita akan mengambil artikel setelah atau sebelum ID artikel tertentu karena ID telah dijamin akan diurutkan dan meningkat.
  • Jenis data yang dimuat - Setiap halaman menampilkan List artikel sehingga jenisnya adalah Article.
  • Sumber data - Biasanya, berupa database, resource jaringan, atau sumber data yang telah dipaginasi lainnya. Namun, dalam kasus codelab ini, kita menggunakan data yang dihasilkan secara lokal.

Dalam paket data, mari kita buat implementasi PagingSource dalam file baru bernama ArticlePagingSource.kt:

package com.example.android.codelabs.paging.data

import androidx.paging.PagingSource
import androidx.paging.PagingState

class ArticlePagingSource : PagingSource<Int, Article>() {
    override suspend fun load(params: LoadParams<Int>): LoadResult<Int, Article> {
        TODO("Not yet implemented")
    }
   override fun getRefreshKey(state: PagingState<Int, Article>): Int? {
        TODO("Not yet implemented")
    }
}

PagingSource mengharuskan kita mengimplementasikan dua fungsi: load() dan getRefreshKey().

Fungsi load() akan dipanggil oleh library Paging untuk secara asinkron mengambil lebih banyak data yang akan ditampilkan saat pengguna melakukan scroll. Objek LoadParams menyimpan informasi terkait operasi pemuatan, termasuk hal berikut:

  • Kunci halaman yang akan dimuat - Jika ini adalah pertama kalinya load() dipanggil, LoadParams.key akan menjadi null. Pada kasus ini, Anda harus menentukan kunci halaman awal. Untuk project, kita menggunakan ID artikel sebagai kunci. Mari tambahkan juga konstanta STARTING_KEY dari 0 ke bagian atas file ArticlePagingSource untuk kunci halaman awal.
  • Ukuran pemuatan - jumlah item yang diminta untuk dimuat.

Fungsi load() akan menampilkan LoadResult. LoadResult dapat berupa salah satu jenis berikut:

  • LoadResult.Page, jika hasilnya berhasil.
  • LoadResult.Error, jika terjadi error.
  • LoadResult.Invalid, jika PagingSource harus dibatalkan validasinya karena tidak dapat lagi menjamin integritas hasilnya.

LoadResult.Page memiliki tiga argumen yang diperlukan:

  • data: List dari item yang diambil.
  • prevKey: Kunci yang digunakan oleh metode load() jika harus mengambil item di belakang halaman saat ini.
  • nextKey: Kunci yang digunakan oleh metode load() jika harus mengambil item setelah halaman saat ini.

...dan dua kunci opsional:

  • itemsBefore: Jumlah placeholder yang ditampilkan sebelum data yang dimuat.
  • itemsAfter: Jumlah placeholder yang akan ditampilkan setelah data dimuat.

Kunci pemuatan kita adalah kolom Article.id. Kita dapat menggunakannya sebagai kunci karena ID Article bertambah satu untuk setiap artikel; yaitu, ID artikel adalah bilangan bulat yang meningkat secara monoton berturut-turut.

nextKey atau prevKey adalah null jika tidak ada lagi data yang dimuat ke arah yang sesuai. Dalam kasus kita, untuk prevKey:

  • Jika startKey sama dengan STARTING_KEY, kita akan menampilkan null karena kita tidak dapat memuat item lainnya di belakang kunci ini.
  • Jika tidak, kita akan mengambil item pertama dalam daftar dan memuat LoadParams.loadSize di belakangnya untuk memastikan agar tidak pernah menampilkan kunci yang kurang dari STARTING_KEY. Kita melakukannya dengan menentukan metode ensureValidKey().

Tambahkan fungsi berikut untuk memastikan apakah kunci paging valid atau tidak:

class ArticlePagingSource : PagingSource<Int, Article>() {
   ...
   /**
     * Makes sure the paging key is never less than [STARTING_KEY]
     */
    private fun ensureValidKey(key: Int) = max(STARTING_KEY, key)
}

Untuk nextKey:

  • Karena kita mendukung pemuatan item tak terbatas, kita meneruskan range.last + 1.

Selain itu, karena setiap artikel memiliki kolom created, kita juga perlu membuat nilai untuknya. Tambahkan berikut ini ke bagian atas file:

private val firstArticleCreatedTime = LocalDateTime.now()

class ArticlePagingSource : PagingSource<Int, Article>() {
   ...
}

Dengan tersedianya semua kode tersebut, sekarang kita dapat mengimplementasikan fungsi load():

import kotlin.math.max
...

private val firstArticleCreatedTime = LocalDateTime.now()

class ArticlePagingSource : PagingSource<Int, Article>() {
   override suspend fun load(params: LoadParams<Int>): LoadResult<Int, Article> {
        // Start paging with the STARTING_KEY if this is the first load
        val start = params.key ?: STARTING_KEY
        // Load as many items as hinted by params.loadSize
        val range = start.until(start + params.loadSize)

        return LoadResult.Page(
            data = range.map { number ->
                Article(
                    // Generate consecutive increasing numbers as the article id
                    id = number,
                    title = "Article $number",
                    description = "This describes article $number",
                    created = firstArticleCreatedTime.minusDays(number.toLong())
                )
            },

            // Make sure we don't try to load items behind the STARTING_KEY
            prevKey = when (start) {
                STARTING_KEY -> null
                else -> ensureValidKey(key = range.first - params.loadSize)
            },
            nextKey = range.last + 1
        )
    }

    ...
}

Berikutnya, kita harus mengimplementasikan getRefreshKey(). Metode ini dipanggil saat library Paging harus memuat ulang item untuk UI karena data dalam PagingSource pendukungnya telah berubah. Situasi saat data dasar untuk PagingSource telah berubah dan perlu diperbarui di UI disebut invalidasi. Jika menjadi tidak valid, Library Paging akan membuat PagingSource baru untuk memuat ulang data, dan memberi tahu UI dengan memunculkan PagingData baru. Kita akan mempelajari lebih lanjut pembatalan validasi di bagian berikutnya.

Saat memuat dari PagingSource baru, getRefreshKey() akan dipanggil untuk menyediakan kunci yang akan digunakan untuk memuat PagingSource baru guna memastikan pengguna tidak kehilangan tempat saat ini dalam daftar setelah dimuat ulang.

Pembatalan dalam library paging terjadi karena salah satu dari dua alasan berikut:

  • Anda memanggil refresh() di PagingAdapter.
  • Anda memanggil invalidate() di PagingSource.

Kunci yang ditampilkan (dalam kasus kita, Int) akan diteruskan ke panggilan berikutnya dari metode load() dalam PagingSource baru melalui argumen LoadParams. Untuk mencegah item menjadi tidak teratur setelah pembatalan validasi, kita harus memastikan jika kunci yang ditampilkan akan memuat item yang cukup untuk mengisi layar. Tindakan ini akan meningkatkan kemungkinan bahwa kumpulan item baru menyertakan item lama dalam data yang tidak tervalidasi sehingga membantu mempertahankan posisi scroll saat ini. Mari lihat implementasinya di aplikasi kita:

   // The refresh key is used for the initial load of the next PagingSource, after invalidation
   override fun getRefreshKey(state: PagingState<Int, Article>): Int? {
        // In our case we grab the item closest to the anchor position
        // then return its id - (state.config.pageSize / 2) as a buffer
        val anchorPosition = state.anchorPosition ?: return null
        val article = state.closestItemToPosition(anchorPosition) ?: return null
        return ensureValidKey(key = article.id - (state.config.pageSize / 2))
    }

Dalam cuplikan di atas, kita menggunakan PagingState.anchorPosition. Jika Anda ingin tahu bagaimana library paging mengetahui cara mengambil item lainnya, ini adalah sebuah petunjuk. Saat mencoba membaca item dari PagingData, UI mencoba membaca pada indeks tertentu. Jika data telah dibaca, data tersebut akan ditampilkan di UI. Namun, jika tidak ada data, library paging tahu bahwa data harus diambil untuk memenuhi permintaan baca yang gagal. Indeks terakhir yang berhasil mengambil data saat dibaca adalah anchorPosition.

Setelah dimuat ulang, kita akan mengambil kunci Article yang terdekat dengan anchorPosition untuk digunakan sebagai kunci pemuatan. Dengan begitu, saat kita mulai memuat lagi dari PagingSource baru, kumpulan item yang diambil akan menyertakan item yang telah dimuat sehingga memastikan pengalaman pengguna yang lancar dan konsisten.

Setelah itu, Anda telah sepenuhnya menentukan PagingSource. Langkah berikutnya adalah menghubungkannya ke UI.

6. Membuat PagingData untuk UI

Dalam implementasi saat ini, Flow<List<Article>> digunakan di ArticleRepository untuk mengekspos data yang dimuat ke ViewModel. Selanjutnya, ViewModel akan mempertahankan status data yang selalu tersedia dengan operator stateIn untuk eksposur ke UI.

Dengan Library Paging, kita akan mengekspos Flow<PagingData<Article>> dari ViewModel. PagingData adalah jenis yang menggabungkan data yang telah dimuat dan membantu library Paging menentukan waktu pengambilan data yang lebih banyak, dan juga memastikan halaman yang sama tidak diminta dua kali.

Untuk membuat PagingData, kita akan menggunakan salah satu dari beberapa metode builder yang berbeda dari class Pager bergantung pada API yang ingin digunakan untuk meneruskan PagingData ke lapisan lain aplikasi kita:

  • Flow Kotlin - gunakan Pager.flow.
  • LiveData - gunakan Pager.liveData.
  • Flowable RxJava - gunakan Pager.flowable.
  • Observable RxJava - gunakan Pager.observable.

Karena Flow sudah digunakan dalam aplikasi, pendekatan ini akan tetap dilanjutkan. Namun, kita akan menggunakan Flow<PagingData<Article>>, dan bukan Flow<List<Article>>.

Apa pun builder PagingData yang akan digunakan, Anda harus meneruskan parameter berikut:

  • PagingConfig. Class ini menetapkan opsi terkait cara memuat konten dari PagingSource seperti seberapa jauh pemuatan, permintaan ukuran untuk pemuatan awal, dan lainnya. Satu-satunya parameter wajib yang harus ditentukan adalah ukuran halaman — berapa banyak item yang harus dimuat di setiap halaman. Secara default, Paging akan menyimpan semua halaman yang dimuat di memori. Untuk memastikan agar tidak menghapus memori saat pengguna men-scroll, setel parameter maxSize di PagingConfig. Jika Paging dapat menghitung item yang tidak dimuat dan jika flag konfigurasi enablePlaceholders bernilai true, Paging secara default akan menampilkan item null sebagai placeholder untuk konten yang belum dimuat. Dengan demikian, Anda akan dapat menampilkan placeholder di adaptor. Untuk menyederhanakan tugas di codelab ini, nonaktifkan placeholder dengan meneruskan enablePlaceholders = false.
  • Fungsi yang menentukan cara membuat PagingSource. Dalam kasus ini, kita akan membuat ArticlePagingSource. Oleh karena itu, kita memerlukan fungsi yang memberi tahu library Paging cara melakukannya.

Mari kita ubah ArticleRepository.

Memperbarui ArticleRepository

  • Hapus kolom articlesStream.
  • Tambahkan metode bernama articlePagingSource() yang menampilkan ArticlePagingSource yang baru saja kita buat.
class ArticleRepository {

    fun articlePagingSource() = ArticlePagingSource()
}

Membersihkan ArticleRepository

Library Paging melakukan banyak hal untuk kita:

  • Menangani cache dalam memori.
  • Meminta data saat pengguna mendekati akhir daftar.

Artinya, semua hal lain di ArticleRepository dapat dihapus, kecuali articlePagingSource(). Sekarang file ArticleRepository Anda akan terlihat seperti ini:

package com.example.android.codelabs.paging.data

import androidx.paging.PagingSource

class ArticleRepository {
    fun articlePagingSource() = ArticlePagingSource()
}

Anda sekarang akan mengompilasi error di ArticleViewModel. Mari kita lihat perubahan apa yang perlu dilakukan!

7. Meminta dan meng-cache PagingData di ViewModel

Sebelum mengatasi error kompilasi, mari kita tinjau ViewModel.

class ArticleViewModel(...) : ViewModel() {

    val items: StateFlow<List<Article>> = ...
}

Untuk mengintegrasikan library Paging di ViewModel, kita harus mengubah jenis nilai items yang ditampilkan dari StateFlow<List<Article>> menjadi Flow<PagingData<Article>>. Untuk melakukannya, pertama-tama tambahkan konstanta pribadi yang disebut ITEMS_PER_PAGE ke bagian atas file:

private const val ITEMS_PER_PAGE = 50

class ArticleViewModel {
    ...
}

Selanjutnya, kita akan memperbarui items sebagai hasil dari output instance Pager. Kita melakukannya dengan meneruskan ke dua parameter Pager:

  • PagingConfig dengan pageSize dari ITEMS_PER_PAGE dan placeholder dinonaktifkan
  • PagingSourceFactory yang menyediakan instance ArticlePagingSource yang baru saja kita buat.
class ArticleViewModel(...) : ViewModel() {

   val items: Flow<PagingData<Article>> = Pager(
        config = PagingConfig(pageSize = ITEMS_PER_PAGE, enablePlaceholders = false),
        pagingSourceFactory = { repository.articlePagingSource() }
    )
        .flow
        ...
}

Selanjutnya, untuk mempertahankan status paging melalui perubahan konfigurasi atau navigasi, kita akan menggunakan metode cachedIn() yang meneruskan androidx.lifecycle.viewModelScope.

Setelah menyelesaikan perubahan di atas, ViewModel akan terlihat seperti ini:

package com.example.android.codelabs.paging.ui

import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import androidx.paging.Pager
import androidx.paging.PagingConfig
import androidx.paging.PagingData
import androidx.paging.cachedIn
import com.example.android.codelabs.paging.data.Article
import com.example.android.codelabs.paging.data.ArticleRepository
import com.example.android.codelabs.paging.data.ITEMS_PER_PAGE
import kotlinx.coroutines.flow.Flow

private const val ITEMS_PER_PAGE = 50

class ArticleViewModel(
    private val repository: ArticleRepository,
) : ViewModel() {

    val items: Flow<PagingData<Article>> = Pager(
        config = PagingConfig(pageSize = ITEMS_PER_PAGE, enablePlaceholders = false),
        pagingSourceFactory = { repository.articlePagingSource() }
    )
        .flow
        .cachedIn(viewModelScope)
}

Hal lain yang perlu diperhatikan tentang PagingData adalah jenis ini berisi jenis pembaruan yang dapat diubah dari data yang akan ditampilkan di RecyclerView. Setiap emisi PagingData benar-benar berdiri sendiri, dan beberapa instance PagingData dapat dimunculkan untuk satu kueri jika PagingSource pendukung tidak valid karena perubahan dalam set data dasar. Dengan demikian, Flows dari PagingData harus ditampilkan secara independen dari Flows lainnya.

Selesai. Sekarang kita memiliki fungsi paging di ViewModel.

8. Membuat Adaptor berfungsi dengan PagingData

Untuk mengikat PagingData ke RecyclerView, gunakan PagingDataAdapter. PagingDataAdapter akan mendapat notifikasi setiap kali konten PagingData dimuat, lalu memberi sinyal ke RecyclerView untuk memperbarui.

Memperbarui ArticleAdapter agar berfungsi dengan aliran PagingData:

  • Saat ini, ArticleAdapter mengimplementasikan ListAdapter. Mari kita buat kode tersebut mengimplementasikan PagingDataAdapter. Isi class lainnya tetap tidak berubah:
import androidx.paging.PagingDataAdapter
...

class ArticleAdapter : PagingDataAdapter<Article, RepoViewHolder>(ARTICLE_DIFF_CALLBACK) {
// body is unchanged
}

Sejauh ini kita telah melakukan banyak perubahan, tetapi sekarang kita hanya selangkah lagi untuk dapat menjalankan aplikasi—hanya perlu menghubungkan UI!

9. Menggunakan PagingData di UI

Dalam implementasi saat ini, kita memiliki metode bernama binding.setupScrollListener() yang memanggil ViewModel untuk memuat lebih banyak data jika kondisi tertentu terpenuhi. Library Paging otomatis melakukan semua ini sehingga kita dapat menghapus metode ini dan penggunaannya.

Selanjutnya, karena ArticleAdapter tidak lagi ListAdapter, tetapi PagingDataAdapter, kita membuat dua perubahan kecil:

  • Kami mengalihkan operator terminal di Flow dari ViewModel ke collectLatest, bukan collect.
  • Kami memberi tahu ArticleAdapter tentang perubahan dengan submitData(), bukan submitList().

Kita menggunakan collectLatest pada pagingData Flow sehingga pengumpulan pada emisi pagingData sebelumnya dibatalkan saat instance pagingData baru dimunculkan.

Dengan perubahan tersebut, Activity akan terlihat seperti ini:

import kotlinx.coroutines.flow.collectLatest

class ArticleActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        val binding = ActivityArticlesBinding.inflate(layoutInflater)
        val view = binding.root
        setContentView(view)

        val viewModel by viewModels<ArticleViewModel>(
            factoryProducer = { Injection.provideViewModelFactory(owner = this) }
        )

        val items = viewModel.items
        val articleAdapter = ArticleAdapter()

        binding.bindAdapter(articleAdapter = articleAdapter)

        lifecycleScope.launch {
            repeatOnLifecycle(Lifecycle.State.STARTED) {
                items.collectLatest {
                    articleAdapter.submitData(it)
                }
            }
        }
    }
}

private fun ActivityArticlesBinding.bindAdapter(
    articleAdapter: ArticleAdapter
) {
    list.adapter = articleAdapter
    list.layoutManager = LinearLayoutManager(list.context)
    val decoration = DividerItemDecoration(list.context, DividerItemDecoration.VERTICAL)
    list.addItemDecoration(decoration)
}

Aplikasi sekarang harus mengompilasi dan berjalan. Anda berhasil memigrasikan aplikasi ke library Paging.

f97136863cfa19a0.gif

10. Menampilkan status pemuatan di UI

Saat library Paging mengambil item lainnya untuk ditampilkan di UI, praktik terbaiknya adalah dengan menunjukkan kepada pengguna bahwa ada lebih banyak data yang akan diproses. Untungnya, library Paging menawarkan cara yang mudah untuk mengakses status pemuatannya dengan jenis CombinedLoadStates.

Instance CombinedLoadStates menjelaskan status pemuatan semua komponen dalam library Paging yang memuat data. Dalam kasus ini, kita hanya tertarik dengan LoadState ArticlePagingSource. Jadi, kita akan menggunakan jenis LoadStates di kolom CombinedLoadStates.source. Anda dapat mengakses CombinedLoadStates melalui PagingDataAdapter melalui PagingDataAdapter.loadStateFlow.

CombinedLoadStates.source merupakan jenis LoadStates dengan kolom untuk tiga jenis LoadState yang berbeda:

  • LoadStates.append: Untuk LoadState item yang diambil setelah posisi pengguna saat ini.
  • LoadStates.prepend: Untuk LoadState item yang diambil sebelum posisi pengguna saat ini.
  • LoadStates.refresh: Untuk LoadState pemuatan awal.

Setiap LoadState bisa menjadi salah satu hal berikut:

  • LoadState.Loading: Item sedang dimuat.
  • LoadState.NotLoading: Item tidak dimuat.
  • LoadState.Error: Terjadi error saat memuat.

Dalam kasus ini, kita hanya memperhatikan jika LoadState adalah LoadState.Loading karena ArticlePagingSource tidak menyertakan kasus error.

Hal pertama yang kita lakukan adalah menambahkan status progres ke bagian atas dan bawah UI untuk menunjukkan status pemuatan untuk pengambilan di kedua arah.

Di activity_articles.xml, tambahkan dua batang LinearProgressIndicator sebagai berikut:

<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".ui.ArticleActivity">

    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/list"
        android:layout_width="0dp"
        android:layout_height="0dp"
        android:scrollbars="vertical"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <com.google.android.material.progressindicator.LinearProgressIndicator
        android:id="@+id/prepend_progress"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:indeterminate="true"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <com.google.android.material.progressindicator.LinearProgressIndicator
        android:id="@+id/append_progress"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:indeterminate="true"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>

Selanjutnya, kita akan merespons CombinedLoadState dengan mengumpulkan LoadStatesFlow dari PagingDataAdapter. Kumpulkan status di ArticleActivity.kt:

override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        ...

        lifecycleScope.launch {
            repeatOnLifecycle(Lifecycle.State.STARTED) {
                articleAdapter.loadStateFlow.collect {
                    binding.prependProgress.isVisible = it.source.prepend is Loading
                    binding.appendProgress.isVisible = it.source.append is Loading
                }
            }
        }
        lifecycleScope.launch {
        ...
    }

Terakhir, kita menambahkan sedikit penundaan di ArticlePagingSource untuk menyimulasikan beban:

private const val LOAD_DELAY_MILLIS = 3_000L

class ArticlePagingSource : PagingSource<Int, Article>() {

    override suspend fun load(params: LoadParams<Int>): LoadResult<Int, Article> {
        val start = params.key ?: STARTING_KEY
        val range = startKey.until(startKey + params.loadSize)

        if (start != STARTING_KEY) delay(LOAD_DELAY_MILLIS)
        return ...

}

Jalankan lagi aplikasi dan scroll ke bagian bawah daftar. Anda akan melihat status progres bawah muncul saat library paging mengambil lebih banyak item dan menghilang jika sudah selesai.

6277154193f7580.gif

11. Menyelesaikan

Mari kita rangkum ringkasan dari pelajaran yang telah kita bahas. Kami telah...:

  • ...mempelajari ringkasan penomoran halaman beserta alasannya.
  • ...menambahkan penomoran halaman ke aplikasi dengan membuat Pager yang menentukan PagingSource dan memunculkan PagingData.
  • ...membuat cache PagingData di ViewModel menggunakan operator cachedIn.
  • ...memakai PagingData di UI menggunakan PagingDataAdapter.
  • ...merespons CombinedLoadStates menggunakan PagingDataAdapter.loadStateFlow.

Selesai. Untuk melihat konsep paging lanjutan lainnya, lihat codelab Paging lanjutan.