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:
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:
Yang akan Anda butuhkan
Opsional
- Pemahaman tentang Komponen Arsitektur berikut: ViewModel, View Binding, dan arsitektur yang disarankan di bagian Panduan Arsitektur Aplikasi. Sebagai pengantar Komponen Arsitektur, lihat Room dengan codelab View.
- Pemahaman tentang coroutine dan Flow Kotlin. Untuk pengantar Flow, lihat codelab Coroutine Lanjutan dengan Flow Kotlin dan LiveData.
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
.
- Buka project di direktori
basic/start
di Android Studio. - Jalankan
app
yang menjalankan konfigurasi di perangkat atau emulator.
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 classDataSource
, lalu olehRepository
untuk digunakan diViewModel
.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 dataPagingData
.PagingSource
memegang kendali untuk melakukan ini dan harus dibuat diViewModel
.PagingData
- penampung untuk data yang telah dipaginasi. Setiap pemuatan ulang data akan memiliki emisiPagingData
terkait yang didukung olehPagingSource
-nya sendiri.PagingDataAdapter
- subclassRecyclerView.Adapter
yang menyajikanPagingData
diRecyclerView
.PagingDataAdapter
dapat dihubungkan keFlow
Kotlin,LiveData
,Flowable
RxJava,Observable
RxJava, atau bahkan daftar statis menggunakan metode factory.PagingDataAdapter
akan memproses peristiwa pemuatanPagingData
internal dan memperbarui UI secara efisien saat halaman dimuat.
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
, danRecyclerView.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 diitems
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 adalahArticle
. - 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 menjadinull
. Pada kasus ini, Anda harus menentukan kunci halaman awal. Untuk project, kita menggunakan ID artikel sebagai kunci. Mari tambahkan juga konstantaSTARTING_KEY
dari0
ke bagian atas fileArticlePagingSource
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
, jikaPagingSource
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 metodeload()
jika harus mengambil item di belakang halaman saat ini.nextKey
: Kunci yang digunakan oleh metodeload()
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 denganSTARTING_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 dariSTARTING_KEY
. Kita melakukannya dengan menentukan metodeensureValidKey()
.
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()
diPagingAdapter
. - Anda memanggil
invalidate()
diPagingSource
.
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 - gunakanPager.flow
.LiveData
- gunakanPager.liveData
.Flowable
RxJava - gunakanPager.flowable
.Observable
RxJava - gunakanPager.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 dariPagingSource
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 parametermaxSize
diPagingConfig
. Jika Paging dapat menghitung item yang tidak dimuat dan jika flag konfigurasienablePlaceholders
bernilaitrue
, 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 meneruskanenablePlaceholders = false
.- Fungsi yang menentukan cara membuat
PagingSource
. Dalam kasus ini, kita akan membuatArticlePagingSource
. 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 menampilkanArticlePagingSource
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
denganpageSize
dariITEMS_PER_PAGE
dan placeholder dinonaktifkanPagingSourceFactory
yang menyediakan instanceArticlePagingSource
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
mengimplementasikanListAdapter
. Mari kita buat kode tersebut mengimplementasikanPagingDataAdapter
. 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
dariViewModel
kecollectLatest
, bukancollect
. - Kami memberi tahu
ArticleAdapter
tentang perubahan dengansubmitData()
, bukansubmitList()
.
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.
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
: UntukLoadState
item yang diambil setelah posisi pengguna saat ini.LoadStates.prepend
: UntukLoadState
item yang diambil sebelum posisi pengguna saat ini.LoadStates.refresh
: UntukLoadState
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.
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 menentukanPagingSource
dan memunculkanPagingData
. - ...membuat cache
PagingData
diViewModel
menggunakan operatorcachedIn
. - ...memakai
PagingData
di UI menggunakanPagingDataAdapter
. - ...merespons
CombinedLoadStates
menggunakanPagingDataAdapter.loadStateFlow
.
Selesai. Untuk melihat konsep paging lanjutan lainnya, lihat codelab Paging lanjutan.