เมื่อทํางานกับข้อมูลแบบแบ่งหน้า คุณมักจะต้องเปลี่ยนรูปแบบสตรีมข้อมูลขณะโหลด เช่น คุณอาจต้องกรองรายการ หรือแปลงรายการเป็นประเภทอื่นก่อนที่จะแสดงใน UI กรณีการใช้งานทั่วไปอีกอย่างหนึ่งสำหรับการเปลี่ยนรูปแบบสตรีมข้อมูลคือการเพิ่มตัวคั่นรายการ
โดยทั่วไปแล้ว การใช้การเปลี่ยนรูปแบบกับสตรีมข้อมูลโดยตรงจะช่วยให้คุณแยกองค์ประกอบของที่เก็บและองค์ประกอบ UI ออกจากกันได้
หน้านี้ถือว่าคุณคุ้นเคยกับการใช้งานพื้นฐานของไลบรารีการแบ่งหน้า
ใช้การเปลี่ยนรูปแบบพื้นฐาน
เนื่องจาก PagingData
ได้รับการบรรจุในสตรีมแบบรีแอกทีฟ คุณจึงใช้การดำเนินการเปลี่ยนรูปแบบกับข้อมูลได้ทีละรายการระหว่างการโหลดข้อมูลและการแสดงข้อมูล
หากต้องการใช้การเปลี่ยนรูปแบบกับออบเจ็กต์ PagingData
แต่ละรายการในสตรีม ให้วางการเปลี่ยนรูปแบบภายในการดำเนินการ map()
บนสตรีม ดังนี้
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. }
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. });
// 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 โดยเฉพาะ ตัวอย่างด้านล่างแสดงวิธีใช้การดำเนินการแมปประเภทนี้
pager.flow // Type is Flow<PagingData<User>>. .map { pagingData -> pagingData.map { user -> UiModel(user) } }
// Type is Flowable<PagingData<User>>. PagingRx.getFlowable(pager) .map(pagingData -> pagingData.map(UiModel.UserModel::new) )
Transformations.map( // Type is LiveData<PagingData<User>>. PagingLiveData.getLiveData(pager), pagingData -> pagingData.map(UiModel.UserModel::new) )
การแปลงข้อมูลทั่วไปอีกอย่างหนึ่งคือการรับอินพุตจากผู้ใช้ เช่น สตริงการค้นหา และแปลงเป็นเอาต์พุตคำขอเพื่อแสดง การตั้งค่านี้ต้องอาศัยการฟังและบันทึกอินพุตคำค้นหาของผู้ใช้ ดำเนินการตามคำขอ และส่งผลลัพธ์การค้นหากลับไปยัง UI
คุณสามารถฟังอินพุตการค้นหาได้โดยใช้สตรีม API เก็บข้อมูลอ้างอิงสตรีมไว้ในViewModel
เลเยอร์ UI ไม่ควรเข้าถึงข้อมูลดังกล่าวโดยตรง แต่ให้กำหนดฟังก์ชันเพื่อแจ้ง ViewModel เกี่ยวกับคำค้นหาของผู้ใช้แทน
private val queryFlow = MutableStateFlow("") fun onQueryChanged(query: String) { queryFlow.value = query }
private BehaviorSubject<String> querySubject = BehaviorSubject.create(""); public void onQueryChanged(String query) { queryFlow.onNext(query) }
private MutableLiveData<String> queryLiveData = new MutableLiveData(""); public void onQueryChanged(String query) { queryFlow.setValue(query) }
เมื่อค่าการค้นหาเปลี่ยนแปลงในสตรีมข้อมูล คุณจะดําเนินการเพื่อแปลงค่าการค้นหาเป็นประเภทข้อมูลที่ต้องการและแสดงผลลัพธ์ไปยังเลเยอร์ UI ได้ ฟังก์ชัน Conversion ที่เฉพาะเจาะจงจะขึ้นอยู่กับภาษาและเฟรมเวิร์กที่ใช้ แต่ฟังก์ชันทั้งหมดจะมีฟังก์ชันการทำงานที่คล้ายกัน
val querySearchResults = queryFlow.flatMapLatest { query -> // The database query returns a Flow which is output through // querySearchResults userDatabase.searchBy(query) }
Observable<User> querySearchResults = querySubject.switchMap(query -> userDatabase.searchBy(query));
LiveData<User> querySearchResults = Transformations.switchMap( queryLiveData, query -> userDatabase.searchBy(query) );
การใช้การดำเนินการอย่าง flatMapLatest
หรือ switchMap
จะช่วยให้มั่นใจได้ว่า UI จะแสดงเฉพาะผลการค้นหาล่าสุดเท่านั้น หากผู้ใช้เปลี่ยนข้อมูลการค้นหาก่อนที่จะดําเนินการฐานข้อมูลเสร็จสมบูรณ์ การดำเนินการเหล่านี้จะทิ้งผลการค้นหาจากคำค้นหาเดิมและเริ่มการค้นหาใหม่ทันที
กรองข้อมูล
การดำเนินการที่พบบ่อยอีกอย่างหนึ่งคือการกรอง คุณสามารถกรองข้อมูลตามเกณฑ์จากผู้ใช้ หรือนำข้อมูลออกจาก UI ได้หากควรซ่อนข้อมูลตามเกณฑ์อื่นๆ
คุณต้องวางการดำเนินการตัวกรองเหล่านี้ไว้ภายในการเรียกใช้ map()
เนื่องจากตัวกรองมีผลกับออบเจ็กต์ PagingData
เมื่อกรองข้อมูลออกจาก PagingData
แล้ว ระบบจะส่งอินสแตนซ์ PagingData
ใหม่ไปยังเลเยอร์ UI เพื่อแสดง
pager.flow // Type is Flow<PagingData<User>>. .map { pagingData -> pagingData.filter { user -> !user.hiddenFromUi } }
// Type is Flowable<PagingData<User>>. PagingRx.getFlowable(pager) .map(pagingData -> pagingData.filter(user -> !user.isHiddenFromUi()) ) }
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
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() }
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; } } }
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>
ที่มีตัวคั่น
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 } } }
// 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; } }); });
// 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()
เพื่อพิจารณารายการในรายการตัวคั่น - ใช้ตัวเปรียบเทียบใหม่
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 }
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); } }
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
pager.flow // Type is Flow<PagingData<User>>. .map { pagingData -> pagingData.filter { user -> !user.hiddenFromUi } .map { user -> UiModel.UserModel(user) } } .cachedIn(viewModelScope)
// 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); }
// 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
แนะนำสำหรับคุณ
โหลดและแสดงข้อมูลการแบ่งหน้า
Discover the latest app development tools, platform updates, training, and documentation for developers across every Android device.
ทดสอบการใช้งานการแบ่งหน้า
Discover the latest app development tools, platform updates, training, and documentation for developers across every Android device.
จัดการและนำเสนอสถานะการโหลด
ดูวิธีติดตามและแสดงสถานะการโหลดสำหรับข้อมูลที่แบ่งหน้าด้วยไลบรารีการแบ่งหน้า