แนวคิดและการใช้งาน Jetpack Compose
เมื่อทำงานกับข้อมูลที่แบ่งหน้า คุณมักจะต้อง เปลี่ยนรูปแบบสตรีมข้อมูลขณะโหลด เช่น คุณอาจต้องกรองรายการ หรือแปลงรายการเป็นประเภทอื่นก่อนที่จะแสดงใน UI กรณีการใช้งานทั่วไปอีกอย่างหนึ่งสำหรับการเปลี่ยนรูปแบบสตรีมข้อมูลคือการเพิ่มตัวคั่นรายการ
โดยทั่วไปแล้ว การใช้การเปลี่ยนรูปแบบกับสตรีมข้อมูลโดยตรงจะช่วยให้คุณ แยกโครงสร้างที่เก็บและโครงสร้าง UI ออกจากกันได้
หน้านี้ถือว่าคุณคุ้นเคยกับการใช้งานพื้นฐานของไลบรารีการแบ่งหน้า
ใช้การเปลี่ยนรูปแบบพื้นฐาน
เนื่องจาก PagingData อยู่ในสตรีมแบบรีแอกทีฟ คุณจึงใช้การดำเนินการเปลี่ยนรูปแบบกับข้อมูลได้ทีละรายการระหว่างการโหลดข้อมูลและการนำเสนอ
หากต้องการใช้การเปลี่ยนรูปแบบกับออบเจ็กต์ PagingData แต่ละรายการในสตรีม
ให้วางการเปลี่ยนรูปแบบไว้ภายใน
map()
การดำเนินการในสตรีม
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
ได้
กรณีการใช้งานทั่วไปอย่างหนึ่งคือการแมปออบเจ็กต์เลเยอร์เครือข่ายหรือฐานข้อมูลกับออบเจ็กต์ที่ใช้ในเลเยอร์ UI โดยเฉพาะ ตัวอย่างด้านล่างแสดงวิธี ใช้การดำเนินการแมปประเภทนี้
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
คุณสามารถฟังอินพุตการค้นหาโดยใช้ Stream API เก็บข้อมูลอ้างอิงสตรีม
ไว้ใน ViewModel เลเยอร์ UI ไม่ควรมีสิทธิ์เข้าถึงโดยตรง แต่ควร
กำหนดฟังก์ชันเพื่อแจ้ง ViewModel เกี่ยวกับคำค้นหาของผู้ใช้แทน
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 ที่เฉพาะเจาะจงจะขึ้นอยู่กับภาษาและเฟรมเวิร์ก ที่ใช้ แต่ทั้งหมดจะให้ฟังก์ชันการทำงานที่คล้ายกัน
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 เพื่อ
แสดง
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 ที่แตกต่างกันได้ วิธีแก้คือการสร้างคลาสที่ปิดผนึกของ Kotlin
พร้อมคลาสย่อยเพื่อแสดงข้อมูลและตัวคั่น หรือคุณจะสร้างคลาสพื้นฐานที่คลาสรายการในลิสต์และคลาสตัวคั่นขยายก็ได้
สมมติว่าคุณต้องการเพิ่มตัวคั่นลงในรายการของUserรายการที่แบ่งหน้า ข้อมูลโค้ดต่อไปนี้แสดงวิธีสร้างคลาสพื้นฐานที่อินสแตนซ์อาจเป็น UserModel หรือ SeparatorModel
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> โดยเพิ่มตัวคั่น
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 เพื่อรองรับประเภทรายการตัวคั่น
สร้างเลย์เอาต์และตัวยึดมุมมองสำหรับรายการตัวคั่น แล้วเปลี่ยน List Adapter ให้ใช้ RecyclerView.ViewHolder เป็นประเภทตัวยึดมุมมอง เพื่อให้สามารถจัดการตัวยึดมุมมองได้มากกว่า 1 ประเภท หรือจะกำหนดคลาสพื้นฐานทั่วไปที่ทั้งคลาสที่ยึดตามรายการและคลาสที่ยึดตามตัวคั่นขยายก็ได้
นอกจากนี้ คุณยังต้องทำการเปลี่ยนแปลงต่อไปนี้กับตัวดัดแปลงรายการด้วย
- เพิ่มเคสไปยังเมธอด
onCreateViewHolder()และonBindViewHolder()เพื่อพิจารณารายการตัวคั่น - ติดตั้งใช้งานเครื่องมือเปรียบเทียบใหม่
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
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
แหล่งข้อมูลเพิ่มเติม
ดูข้อมูลเพิ่มเติมเกี่ยวกับไลบรารี Paging ได้ที่แหล่งข้อมูลเพิ่มเติมต่อไปนี้
Codelabs
แนะนำสำหรับคุณ
- หมายเหตุ: ข้อความลิงก์จะแสดงเมื่อ JavaScript ปิดอยู่
- โหลดและแสดงข้อมูลแบบแบ่งหน้า
- ทดสอบการใช้งานการแบ่งหน้า
- จัดการและแสดงสถานะการโหลด