Saat bekerja dengan data yang di-page, aliran data sering kali perlu ditransformasi saat Anda memuatnya. Misalnya, Anda mungkin perlu memfilter daftar item atau mengonversi item menjadi jenis yang berbeda sebelum menyajikannya dalam UI. Kasus penggunaan umum lainnya terkait transformasi aliran data adalah menambahkan pemisah daftar.
Secara lebih umum, penerapan transformasi ke aliran data secara langsung akan memungkinkan Anda memisahkan konstruksi repositori dan UI.
Halaman ini mengasumsikan bahwa Anda telah memahami penggunaan dasar library Paging.
Menerapkan transformasi dasar
Karena PagingData
dienkapsulasi dalam aliran reaktif, Anda dapat menerapkan operasi transformasi
pada data secara bertahap antara proses pemuatan dan penyajian data.
Untuk menerapkan transformasi ke setiap objek PagingData
di dalam aliran,
tempatkan transformasi di dalam operasi
map()
di aliran tersebut:
Kotlin
pager.flow // Type is Flow<PagingData<User>>. // Map the outer stream so that the transformations are applied to // each new generation of PagingData. .map { pagingData -> // Transformations in this block are applied to the items // in the paged data. }
Java
PagingRx.getFlowable(pager) // Type is Flowable<PagingData<User>>. // Map the outer stream so that the transformations are applied to // each new generation of PagingData. .map(pagingData -> { // Transformations in this block are applied to the items // in the paged data. });
Java
// Map the outer stream so that the transformations are applied to // each new generation of PagingData. Transformations.map( // Type is LiveData<PagingData<User>>. PagingLiveData.getLiveData(pager), pagingData -> { // Transformations in this block are applied to the items // in the paged data. });
Mengonversi data
Operasi paling dasar pada aliran data adalah mengonversinya ke jenis
lain. Setelah memiliki akses ke objek PagingData
, Anda dapat menjalankan operasi map()
pada setiap item individual dalam daftar yang di-page dalam objek
PagingData
.
Salah satu kasus penggunaan umum untuk hal ini adalah memetakan objek lapisan database atau jaringan ke objek yang secara khusus digunakan di lapisan UI. Contoh di bawah menunjukkan cara menerapkan jenis operasi peta ini:
Kotlin
pager.flow // Type is Flow<PagingData<User>>. .map { pagingData -> pagingData.map { user -> UiModel(user) } }
Java
// Type is Flowable<PagingData<User>>. PagingRx.getFlowable(pager) .map(pagingData -> pagingData.map(UiModel.UserModel::new) )
Java
Transformations.map( // Type is LiveData<PagingData<User>>. PagingLiveData.getLiveData(pager), pagingData -> pagingData.map(UiModel.UserModel::new) )
Konversi data umum lainnya adalah menggunakan input dari pengguna, seperti string kueri, dan mengonversinya menjadi output permintaan untuk ditampilkan. Untuk menyiapkannya, Anda harus memproses dan merekam input kueri pengguna, melakukan permintaannya, dan mengirimkan hasil kuerinya kembali ke UI.
Anda dapat memproses input kueri menggunakan API aliran data. Simpan referensi aliran data
dalam ViewModel
. Lapisan UI tidak boleh memiliki akses langsung ke input tersebut; sebagai gantinya,
tetapkan fungsi untuk memberi tahu ViewModel tentang kueri pengguna.
Kotlin
private val queryFlow = MutableStateFlow("") fun onQueryChanged(query: String) { queryFlow.value = query }
Java
private BehaviorSubject<String> querySubject = BehaviorSubject.create(""); public void onQueryChanged(String query) { queryFlow.onNext(query) }
Java
private MutableLiveData<String> queryLiveData = new MutableLiveData(""); public void onQueryChanged(String query) { queryFlow.setValue(query) }
Saat nilai kueri berubah dalam aliran data, Anda dapat menjalankan operasi untuk mengonversi nilai kueri ke jenis data yang diinginkan dan menampilkan hasilnya ke lapisan UI. Fungsi konversi khusus bergantung pada bahasa dan framework yang digunakan, tetapi semuanya menyediakan fungsi yang serupa.
Kotlin
val querySearchResults = queryFlow.flatMapLatest { query -> // The database query returns a Flow which is output through // querySearchResults userDatabase.searchBy(query) }
Java
Observable<User> querySearchResults = querySubject.switchMap(query -> userDatabase.searchBy(query));
Java
LiveData<User> querySearchResults = Transformations.switchMap( queryLiveData, query -> userDatabase.searchBy(query) );
Menggunakan operasi seperti flatMapLatest
atau switchMap
memastikan bahwa hanya
hasil terbaru yang akan ditampilkan ke UI. Jika pengguna mengubah input kueri
sebelum operasi database selesai, operasi ini akan membuang hasil
dari kueri lama dan segera meluncurkan penelusuran baru.
Memfilter data
Operasi umum lainnya adalah pemfilteran. Anda dapat memfilter data berdasarkan kriteria dari pengguna, atau Anda dapat menghapus data dari UI jika data harus disembunyikan berdasarkan kriteria lain.
Anda harus menempatkan operasi filter ini dalam panggilan map()
karena
filternya berlaku untuk objek PagingData
. Setelah data difilter dari
PagingData
, instance PagingData
baru akan diteruskan ke lapisan UI untuk
ditampilkan.
Kotlin
pager.flow // Type is Flow<PagingData<User>>. .map { pagingData -> pagingData.filter { user -> !user.hiddenFromUi } }
Java
// Type is Flowable<PagingData<User>>. PagingRx.getFlowable(pager) .map(pagingData -> pagingData.filter(user -> !user.isHiddenFromUi()) ) }
Java
Transformations.map( // Type is LiveData<PagingData<User>>. PagingLiveData.getLiveData(pager), pagingData -> pagingData.filter(user -> !user.isHiddenFromUi()) )
Menambahkan pemisah daftar
Library Paging mendukung pemisah daftar dinamis. Anda dapat meningkatkan
keterbacaan daftar dengan memasukkan pemisah secara langsung ke aliran data sebagai
item daftar RecyclerView
. Akibatnya, pemisah menjadi objek
ViewHolder
berfitur lengkap, yang memungkinkan interaktivitas, fokus aksesibilitas, serta semua
fitur lainnya yang disediakan oleh View
.
Ada tiga langkah saat Anda memasukan separator ke dalam daftar yang di-page:
- Mengonversi model UI untuk mengakomodasi item pemisah.
- Mentransformasi aliran data untuk menambahkan pemisah antara proses pemuatan dan penyajian data secara dinamis.
- Mengupdate UI untuk menangani item pemisah.
Mengonversi model UI
Library Paging akan memasukkan pemisah daftar ke dalam RecyclerView
sebagai
item daftar yang sebenarnya, tetapi item pemisah harus dapat dibedakan dari item data
dalam daftar agar dapat diikat ke jenis ViewHolder
yang berbeda dengan
UI berbeda. Solusinya adalah dengan membuat class
terbatas dalam Kotlin
dengan subclass untuk mewakili data dan pemisah Anda. Selain itu, Anda
juga dapat membuat class dasar yang diperpanjang oleh class item daftar dan
class pemisah.
Anggaplah Anda ingin menambahkan pemisah ke daftar item User
yang di-page. Cuplikan
berikut menunjukkan cara pembuatan class dasar saat instancenya
berupa UserModel
atau SeparatorModel
:
Kotlin
sealed class UiModel { class UserModel(val id: String, val label: String) : UiModel() { constructor(user: User) : this(user.id, user.label) } class SeparatorModel(val description: String) : UiModel() }
Java
class UiModel { private UiModel() {} static class UserModel extends UiModel { @NonNull private String mId; @NonNull private String mLabel; UserModel(@NonNull String id, @NonNull String label) { mId = id; mLabel = label; } UserModel(@NonNull User user) { mId = user.id; mLabel = user.label; } @NonNull public String getId() { return mId; } @NonNull public String getLabel() { return mLabel; } } static class SeparatorModel extends UiModel { @NonNull private String mDescription; SeparatorModel(@NonNull String description) { mDescription = description; } @NonNull public String getDescription() { return mDescription; } } }
Java
class UiModel { private UiModel() {} static class UserModel extends UiModel { @NonNull private String mId; @NonNull private String mLabel; UserModel(@NonNull String id, @NonNull String label) { mId = id; mLabel = label; } UserModel(@NonNull User user) { mId = user.id; mLabel = user.label; } @NonNull public String getId() { return mId; } @NonNull public String getLabel() { return mLabel; } } static class SeparatorModel extends UiModel { @NonNull private String mDescription; SeparatorModel(@NonNull String description) { mDescription = description; } @NonNull public String getDescription() { return mDescription; } } }
Mentransformasi aliran data
Anda harus melakukan transformasi aliran data setelah memuat dan sebelum menampilkannya. Transformasi harus melakukan hal berikut:
- Mengonversi item daftar yang dimuat untuk merefleksikan jenis item dasar yang baru.
- Menggunakan metode
PagingData.insertSeparators()
untuk menambahkan pemisah.
Untuk mempelajari operasi transformasi lebih lanjut, lihat Menerapkan transformasi dasar.
Contoh berikut menunjukkan operasi transformasi untuk memperbarui
aliran PagingData<User>
menjadi aliran PagingData<UiModel>
dengan pemisah yang
ditambahkan:
Kotlin
pager.flow.map { pagingData: PagingData<User> -> // Map outer stream, so you can perform transformations on // each paging generation. pagingData .map { user -> // Convert items in stream to UiModel.UserModel. UiModel.UserModel(user) } .insertSeparators<UiModel.UserModel, UiModel> { before, after -> when { before == null -> UiModel.SeparatorModel("HEADER") after == null -> UiModel.SeparatorModel("FOOTER") shouldSeparate(before, after) -> UiModel.SeparatorModel( "BETWEEN ITEMS $before AND $after" ) // Return null to avoid adding a separator between two items. else -> null } } }
Java
// Map outer stream, so you can perform transformations on each // paging generation. PagingRx.getFlowable(pager).map(pagingData -> { // First convert items in stream to UiModel.UserModel. PagingData<UiModel> uiModelPagingData = pagingData.map( UiModel.UserModel::new); // Insert UiModel.SeparatorModel, which produces PagingData of // generic type UiModel. return PagingData.insertSeparators(uiModelPagingData, (@Nullable UiModel before, @Nullable UiModel after) -> { if (before == null) { return new UiModel.SeparatorModel("HEADER"); } else if (after == null) { return new UiModel.SeparatorModel("FOOTER"); } else if (shouldSeparate(before, after)) { return new UiModel.SeparatorModel("BETWEEN ITEMS " + before.toString() + " AND " + after.toString()); } else { // Return null to avoid adding a separator between two // items. return null; } }); });
Java
// Map outer stream, so you can perform transformations on each // paging generation. Transformations.map(PagingLiveData.getLiveData(pager), pagingData -> { // First convert items in stream to UiModel.UserModel. PagingData<UiModel> uiModelPagingData = pagingData.map( UiModel.UserModel::new); // Insert UiModel.SeparatorModel, which produces PagingData of // generic type UiModel. return PagingData.insertSeparators(uiModelPagingData, (@Nullable UiModel before, @Nullable UiModel after) -> { if (before == null) { return new UiModel.SeparatorModel("HEADER"); } else if (after == null) { return new UiModel.SeparatorModel("FOOTER"); } else if (shouldSeparate(before, after)) { return new UiModel.SeparatorModel("BETWEEN ITEMS " + before.toString() + " AND " + after.toString()); } else { // Return null to avoid adding a separator between two // items. return null; } }); });
Menangani pemisah di UI
Langkah terakhir adalah mengubah UI agar mengakomodasi jenis item pemisah.
Buat tata letak dan penampung tampilan untuk item pemisah Anda, lalu ubah
adaptor daftar agar menggunakan RecyclerView.ViewHolder
sebagai jenis penampung tampilannya
sehingga dapat menangani lebih dari satu jenis penampung tampilan. Selain itu, Anda dapat menentukan class dasar umum
yang diperluas class penunjang tampilan pemisah dan item.
Anda juga harus membuat perubahan berikut pada adaptor daftar:
- Menambahkan kasus ke metode
onCreateViewHolder()
danonBindViewHolder()
untuk menjelaskan item daftar pemisah. - Mengimplementasikan pembanding baru.
Kotlin
class UiModelAdapter : PagingDataAdapter<UiModel, RecyclerView.ViewHolder>(UiModelComparator) { override fun onCreateViewHolder( parent: ViewGroup, viewType: Int ) = when (viewType) { R.layout.item -> UserModelViewHolder(parent) else -> SeparatorModelViewHolder(parent) } override fun getItemViewType(position: Int) { // Use peek over getItem to avoid triggering page fetch / drops, since // recycling views is not indicative of the user's current scroll position. return when (peek(position)) { is UiModel.UserModel -> R.layout.item is UiModel.SeparatorModel -> R.layout.separator_item null -> throw IllegalStateException("Unknown view") } } override fun onBindViewHolder( holder: RecyclerView.ViewHolder, position: Int ) { val item = getItem(position) if (holder is UserModelViewHolder) { holder.bind(item as UserModel) } else if (holder is SeparatorModelViewHolder) { holder.bind(item as SeparatorModel) } } } object UiModelComparator : DiffUtil.ItemCallback<UiModel>() { override fun areItemsTheSame( oldItem: UiModel, newItem: UiModel ): Boolean { val isSameRepoItem = oldItem is UiModel.UserModel && newItem is UiModel.UserModel && oldItem.id == newItem.id val isSameSeparatorItem = oldItem is UiModel.SeparatorModel && newItem is UiModel.SeparatorModel && oldItem.description == newItem.description return isSameRepoItem || isSameSeparatorItem } override fun areContentsTheSame( oldItem: UiModel, newItem: UiModel ) = oldItem == newItem }
Java
class UiModelAdapter extends PagingDataAdapter<UiModel, RecyclerView.ViewHolder> { UiModelAdapter() { super(new UiModelComparator(), Dispatchers.getMain(), Dispatchers.getDefault()); } @NonNull @Override public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { if (viewType == R.layout.item) { return new UserModelViewHolder(parent); } else { return new SeparatorModelViewHolder(parent); } } @Override public int getItemViewType(int position) { // Use peek over getItem to avoid triggering page fetch / drops, since // recycling views is not indicative of the user's current scroll position. UiModel item = peek(position); if (item instanceof UiModel.UserModel) { return R.layout.item; } else if (item instanceof UiModel.SeparatorModel) { return R.layout.separator_item; } else { throw new IllegalStateException("Unknown view"); } } @Override public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) { if (holder instanceOf UserModelViewHolder) { UserModel userModel = (UserModel) getItem(position); ((UserModelViewHolder) holder).bind(userModel); } else { SeparatorModel separatorModel = (SeparatorModel) getItem(position); ((SeparatorModelViewHolder) holder).bind(separatorModel); } } } class UiModelComparator extends DiffUtil.ItemCallback<UiModel> { @Override public boolean areItemsTheSame(@NonNull UiModel oldItem, @NonNull UiModel newItem) { boolean isSameRepoItem = oldItem instanceof UserModel && newItem instanceof UserModel && ((UserModel) oldItem).getId().equals(((UserModel) newItem).getId()); boolean isSameSeparatorItem = oldItem instanceof SeparatorModel && newItem instanceof SeparatorModel && ((SeparatorModel) oldItem).getDescription().equals( ((SeparatorModel) newItem).getDescription()); return isSameRepoItem || isSameSeparatorItem; } @Override public boolean areContentsTheSame(@NonNull UiModel oldItem, @NonNull UiModel newItem) { return oldItem.equals(newItem); } }
Java
class UiModelAdapter extends PagingDataAdapter<UiModel, RecyclerView.ViewHolder> { UiModelAdapter() { super(new UiModelComparator(), Dispatchers.getMain(), Dispatchers.getDefault()); } @NonNull @Override public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { if (viewType == R.layout.item) { return new UserModelViewHolder(parent); } else { return new SeparatorModelViewHolder(parent); } } @Override public int getItemViewType(int position) { // Use peek over getItem to avoid triggering page fetch / drops, since // recycling views is not indicative of the user's current scroll position. UiModel item = peek(position); if (item instanceof UiModel.UserModel) { return R.layout.item; } else if (item instanceof UiModel.SeparatorModel) { return R.layout.separator_item; } else { throw new IllegalStateException("Unknown view"); } } @Override public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) { if (holder instanceOf UserModelViewHolder) { UserModel userModel = (UserModel) getItem(position); ((UserModelViewHolder) holder).bind(userModel); } else { SeparatorModel separatorModel = (SeparatorModel) getItem(position); ((SeparatorModelViewHolder) holder).bind(separatorModel); } } } class UiModelComparator extends DiffUtil.ItemCallback<UiModel> { @Override public boolean areItemsTheSame(@NonNull UiModel oldItem, @NonNull UiModel newItem) { boolean isSameRepoItem = oldItem instanceof UserModel && newItem instanceof UserModel && ((UserModel) oldItem).getId().equals(((UserModel) newItem).getId()); boolean isSameSeparatorItem = oldItem instanceof SeparatorModel && newItem instanceof SeparatorModel && ((SeparatorModel) oldItem).getDescription().equals( ((SeparatorModel) newItem).getDescription()); return isSameRepoItem || isSameSeparatorItem; } @Override public boolean areContentsTheSame(@NonNull UiModel oldItem, @NonNull UiModel newItem) { return oldItem.equals(newItem); } }
Menghindari tugas duplikat
Satu masalah utama yang harus dihindari adalah membuat aplikasi melakukan tugas yang tidak perlu. Pengambilan data adalah operasi yang mahal, dan transformasi data juga dapat menghabiskan waktu yang berharga. Setelah dimuat dan disiapkan untuk ditampilkan di UI, data harus disimpan jika perubahan konfigurasi terjadi dan UI harus dibuat ulang.
Operasi cachedIn()
akan meng-cache hasil transformasi apa pun yang terjadi
sebelumnya. Oleh karena itu, cachedIn()
harus menjadi panggilan terakhir dalam ViewModel Anda.
Kotlin
pager.flow // Type is Flow<PagingData<User>>. .map { pagingData -> pagingData.filter { user -> !user.hiddenFromUi } .map { user -> UiModel.UserModel(user) } } .cachedIn(viewModelScope)
Java
// CoroutineScope helper provided by the lifecycle-viewmodel-ktx artifact. CoroutineScope viewModelScope = ViewModelKt.getViewModelScope(viewModel); PagingRx.cachedIn( // Type is Flowable<PagingData<User>>. PagingRx.getFlowable(pager) .map(pagingData -> pagingData .filter(user -> !user.isHiddenFromUi()) .map(UiModel.UserModel::new)), viewModelScope); }
Java
// CoroutineScope helper provided by the lifecycle-viewmodel-ktx artifact. CoroutineScope viewModelScope = ViewModelKt.getViewModelScope(viewModel); PagingLiveData.cachedIn( Transformations.map( // Type is LiveData<PagingData<User>>. PagingLiveData.getLiveData(pager), pagingData -> pagingData .filter(user -> !user.isHiddenFromUi()) .map(UiModel.UserModel::new)), viewModelScope);
Untuk informasi selengkapnya tentang cara menggunakan cachedIn()
dengan aliran data PagingData
, lihat
Menyiapkan aliran data
PagingData.
Referensi lainnya
Untuk mempelajari library Paging lebih lanjut, lihat referensi tambahan berikut:
Codelab
Direkomendasikan untuk Anda
- Catatan: teks link ditampilkan saat JavaScript nonaktif
- Memuat dan menampilkan data yang dibagi-bagi
- Menguji implementasi Paging
- Mengelola dan menampilkan status pemuatan