เมื่อทํางานกับข้อมูลแบบแบ่งหน้า คุณมักจะต้องเปลี่ยนรูปแบบสตรีมข้อมูลขณะโหลด เช่น คุณอาจต้องกรองรายการ หรือแปลงรายการเป็นประเภทอื่นก่อนที่จะแสดงใน UI กรณีการใช้งานทั่วไปอีกอย่างหนึ่งสำหรับการเปลี่ยนรูปแบบสตรีมข้อมูลคือการเพิ่มตัวคั่นรายการ
โดยทั่วไปแล้ว การใช้การเปลี่ยนรูปแบบกับสตรีมข้อมูลโดยตรงจะช่วยให้คุณแยกองค์ประกอบของที่เก็บและองค์ประกอบ UI ออกจากกันได้
หน้านี้ถือว่าคุณคุ้นเคยกับการใช้งานพื้นฐานของไลบรารีการแบ่งหน้า
ใช้การเปลี่ยนรูปแบบพื้นฐาน
เนื่องจาก PagingData
ได้รับการบรรจุในสตรีมแบบรีแอกทีฟ คุณจึงใช้การดำเนินการเปลี่ยนรูปแบบกับข้อมูลได้ทีละรายการระหว่างการโหลดข้อมูลและการแสดงข้อมูล
หากต้องการใช้การเปลี่ยนรูปแบบกับออบเจ็กต์ PagingData
แต่ละรายการในสตรีม ให้วางการเปลี่ยนรูปแบบภายในการดำเนินการ map()
บนสตรีม ดังนี้
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. });
แปลงข้อมูล
การดำเนินการขั้นพื้นฐานที่สุดกับสตรีมข้อมูลคือการแปลงข้อมูลเป็นสตรีมประเภทอื่น เมื่อเข้าถึงออบเจ็กต์ PagingData
แล้ว คุณจะดำเนินการ map()
ได้กับแต่ละรายการในรายการแบบแบ่งหน้าภายในออบเจ็กต์ PagingData
Use Case ทั่วไปอย่างหนึ่งของการดำเนินการนี้คือเพื่อจับคู่ออบเจ็กต์เลเยอร์เครือข่ายหรือฐานข้อมูลกับออบเจ็กต์ที่ใช้ในเลเยอร์ UI โดยเฉพาะ ตัวอย่างด้านล่างแสดงวิธีใช้การดำเนินการแมปประเภทนี้
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) )
การแปลงข้อมูลทั่วไปอีกอย่างหนึ่งคือการรับอินพุตจากผู้ใช้ เช่น สตริงการค้นหา และแปลงเป็นเอาต์พุตคำขอเพื่อแสดง การตั้งค่านี้ต้องอาศัยการฟังและบันทึกอินพุตคำค้นหาของผู้ใช้ ดำเนินการตามคำขอ และส่งผลลัพธ์การค้นหากลับไปยัง UI
คุณสามารถฟังอินพุตการค้นหาได้โดยใช้สตรีม API เก็บข้อมูลอ้างอิงสตรีมไว้ในViewModel
เลเยอร์ UI ไม่ควรเข้าถึงข้อมูลดังกล่าวโดยตรง แต่ให้กำหนดฟังก์ชันเพื่อแจ้ง ViewModel เกี่ยวกับคำค้นหาของผู้ใช้แทน
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) }
เมื่อค่าการค้นหาเปลี่ยนแปลงในสตรีมข้อมูล คุณจะดําเนินการเพื่อแปลงค่าการค้นหาเป็นประเภทข้อมูลที่ต้องการและแสดงผลลัพธ์ไปยังเลเยอร์ UI ได้ ฟังก์ชัน Conversion ที่เฉพาะเจาะจงจะขึ้นอยู่กับภาษาและเฟรมเวิร์กที่ใช้ แต่ฟังก์ชันทั้งหมดจะมีฟังก์ชันการทำงานที่คล้ายกัน
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) );
การใช้การดำเนินการอย่าง flatMapLatest
หรือ switchMap
จะช่วยให้มั่นใจได้ว่า UI จะแสดงเฉพาะผลการค้นหาล่าสุดเท่านั้น หากผู้ใช้เปลี่ยนข้อมูลการค้นหาก่อนที่จะดําเนินการฐานข้อมูลเสร็จสมบูรณ์ การดำเนินการเหล่านี้จะทิ้งผลการค้นหาจากคำค้นหาเดิมและเริ่มการค้นหาใหม่ทันที
กรองข้อมูล
การดำเนินการที่พบบ่อยอีกอย่างหนึ่งคือการกรอง คุณสามารถกรองข้อมูลตามเกณฑ์จากผู้ใช้ หรือนำข้อมูลออกจาก UI ได้หากควรซ่อนข้อมูลตามเกณฑ์อื่นๆ
คุณต้องวางการดำเนินการตัวกรองเหล่านี้ไว้ภายในการเรียกใช้ map()
เนื่องจากตัวกรองมีผลกับออบเจ็กต์ PagingData
เมื่อกรองข้อมูลออกจาก PagingData
แล้ว ระบบจะส่งอินสแตนซ์ PagingData
ใหม่ไปยังเลเยอร์ UI เพื่อแสดง
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()) )
เพิ่มตัวแบ่งรายการ
ไลบรารีการแบ่งหน้ารองรับตัวคั่นรายการแบบไดนามิก คุณสามารถปรับปรุงความง่ายในการอ่านรายการได้โดยแทรกตัวคั่นลงในสตรีมข้อมูลโดยตรงเป็นRecyclerView
รายการในรายการ ด้วยเหตุนี้ ตัวคั่นจึงเป็นออบเจ็กต์ ViewHolder
ที่มีฟีเจอร์ครบถ้วน ซึ่งช่วยให้มีการโต้ตอบ โฟกัสการช่วยเหลือพิเศษ และฟีเจอร์อื่นๆ ทั้งหมดที่ View
มีให้
การแทรกตัวคั่นลงในรายการแบบแบ่งหน้ามี 3 ขั้นตอนดังนี้
- แปลงโมเดล UI ให้รองรับรายการตัวคั่น
- เปลี่ยนรูปแบบสตรีมข้อมูลเพื่อเพิ่มตัวคั่นระหว่างการโหลดข้อมูลกับการแสดงข้อมูลแบบไดนามิก
- อัปเดต UI เพื่อจัดการรายการตัวคั่น
แปลงโมเดล UI
ไลบรารีการแบ่งหน้าจะแทรกตัวคั่นรายการลงใน RecyclerView
เป็นรายการรายการจริง แต่รายการตัวคั่นต้องแยกความแตกต่างจากรายการข้อมูลในรายการเพื่อให้สามารถเชื่อมโยงกับ ViewHolder
ประเภทอื่นที่มี UI ที่แตกต่างกัน วิธีแก้ปัญหาคือสร้างคลาสแบบ Sealed ของ Kotlin ที่มีคลาสย่อยเพื่อแสดงข้อมูลและตัวคั่น หรือจะสร้างคลาสพื้นฐานที่ขยายโดยคลาสรายการลิสต์และคลาสตัวคั่นก็ได้
สมมติว่าคุณต้องการเพิ่มตัวคั่นลงในรายการ User
รายการแบบแบ่งหน้า ข้อมูลโค้ดต่อไปนี้แสดงวิธีสร้างคลาสพื้นฐานที่อินสแตนซ์อาจเป็น UserModel
หรือ 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; } } }
เปลี่ยนรูปแบบสตรีมข้อมูล
คุณต้องเปลี่ยนรูปแบบสตรีมข้อมูลหลังจากโหลดและก่อนแสดง การเปลี่ยนรูปแบบควรทําสิ่งต่อไปนี้
- แปลงรายการในรายการที่โหลดเพื่อให้สอดคล้องกับประเภทรายการพื้นฐานใหม่
- ใช้เมธอด
PagingData.insertSeparators()
เพื่อเพิ่มตัวคั่น
ดูข้อมูลเพิ่มเติมเกี่ยวกับการดําเนินการเปลี่ยนรูปแบบได้ที่ใช้การเปลี่ยนรูปแบบพื้นฐาน
ตัวอย่างต่อไปนี้แสดงการดำเนินการเปลี่ยนรูปแบบเพื่ออัปเดตสตรีม PagingData<User>
เป็นสตรีม PagingData<UiModel>
ที่มีตัวคั่น
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; } }); });
จัดการตัวคั่นใน UI
ขั้นตอนสุดท้ายคือเปลี่ยน UI ให้รองรับประเภทรายการตัวคั่น
สร้างเลย์เอาต์และตัวยึดตำแหน่งสำหรับรายการตัวคั่น และเปลี่ยนอะแดปเตอร์รายการเพื่อใช้ RecyclerView.ViewHolder
เป็นประเภทตัวยึดตำแหน่งเพื่อให้จัดการตัวยึดตำแหน่งได้มากกว่า 1 ประเภท หรือจะกําหนดคลาสพื้นฐานทั่วไปที่ทั้งคลาสตัวยึดตําแหน่งรายการและตัวยึดตําแหน่งตัวคั่นจะขยายก็ได้
นอกจากนี้ คุณต้องทำการเปลี่ยนแปลงต่อไปนี้กับอะแดปเตอร์รายการด้วย
- เพิ่มเคสลงในเมธอด
onCreateViewHolder()
และonBindViewHolder()
เพื่อพิจารณารายการในรายการตัวคั่น - ใช้ตัวเปรียบเทียบใหม่
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); } }
หลีกเลี่ยงการทำงานซ้ำ
ปัญหาสำคัญอย่างหนึ่งที่ควรหลีกเลี่ยงคือการทำให้แอปทำงานที่ไม่จำเป็น การดึงข้อมูลเป็นการดำเนินการที่มีค่าใช้จ่ายสูง และการแปลงข้อมูลยังอาจใช้เวลาอันมีค่าด้วย เมื่อโหลดข้อมูลและเตรียมข้อมูลให้แสดงใน UI แล้ว คุณควรบันทึกข้อมูลไว้ในกรณีที่มีการเปลี่ยนแปลงการกําหนดค่าและจําเป็นต้องสร้าง UI ขึ้นมาใหม่
การดำเนินการ cachedIn()
จะแคชผลลัพธ์ของการเปลี่ยนรูปแบบที่เกิดขึ้นก่อนหน้า ดังนั้น cachedIn()
จึงควรเป็นการเรียกใช้ครั้งสุดท้ายใน ViewModel
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);
ดูข้อมูลเพิ่มเติมเกี่ยวกับการใช้ cachedIn()
กับสตรีมของ PagingData
ได้ที่หัวข้อตั้งค่าสตรีมของ PagingData
แหล่งข้อมูลเพิ่มเติม
ดูข้อมูลเพิ่มเติมเกี่ยวกับไลบรารีการแบ่งหน้าได้ที่แหล่งข้อมูลเพิ่มเติมต่อไปนี้
Codelabs
แนะนำสำหรับคุณ
- หมายเหตุ: ข้อความลิงก์จะแสดงเมื่อ JavaScript ปิดอยู่
- โหลดและแสดงข้อมูลที่แบ่งหน้า
- ทดสอบการใช้งานการแบ่งหน้า
- จัดการและแสดงสถานะการโหลด