כשעובדים עם נתונים שמחולקים לדפים, לעיתים קרובות צריך לבצע טרנספורמציה של מקור הנתונים בזמן הטעינה שלו. לדוגמה, יכול להיות שתצטרכו לסנן רשימה של פריטים או להמיר פריטים לסוג אחר לפני שתציגו אותם בממשק המשתמש. תרחיש לדוגמה נוסף שבו נעשה שימוש בטרנספורמציה של מקור נתונים הוא הוספת מפרידים של רשימות.
באופן כללי, החלת טרנספורמציות ישירות על מקור הנתונים מאפשרת לכם להפריד בין המבנים של המאגר לבין המבנים של ממשק המשתמש.
בדף הזה נדרש ידע בשימוש בסיסי בספריית הגיליון.
החלת טרנספורמציות בסיסיות
מכיוון ש-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
.
תרחיש שימוש נפוץ לכך הוא מיפוי של אובייקט בשכבת הרשת או בשכבת מסד הנתונים לאובייקט שמשמש באופן ספציפי בשכבת ממשק המשתמש. בדוגמה הבאה מוסבר איך מחילים את סוג הפעולה הזה במיפוי:
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) )
המרה נפוצה נוספת של נתונים היא קבלת קלט מהמשתמש, כמו מחרוזת שאילתה, והמרה שלו לפלט הבקשה להצגה. כדי להגדיר את זה, צריך להאזין לקלט של שאילתת המשתמש ולתעד אותו, לבצע את הבקשה ולהעביר את תוצאת השאילתה חזרה לממשק המשתמש.
אפשר להאזין לקלט של השאילתה באמצעות API של מקור נתונים. שומרים את ההפניה לשידור ב-ViewModel
. שכבת ממשק המשתמש לא צריכה לקבל גישה ישירה אליו. במקום זאת, צריך להגדיר פונקציה שמודיעה ל-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) }
כשערך השאילתה משתנה במקור הנתונים, אפשר לבצע פעולות כדי להמיר את ערך השאילתה לסוג הנתונים הרצוי ולהחזיר את התוצאה לשכבת ממשק המשתמש. פונקציית ההמרה הספציפית תלויה בשפה ובמסגרת שבהן משתמשים, אבל כולן מספקות פונקציונליות דומה.
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
מבטיח שרק התוצאות העדכניות ביותר יחזרו לממשק המשתמש. אם המשתמש משנה את הקלט של השאילתה לפני שהפעולה במסד הנתונים מסתיימת, הפעולות האלה מבטלות את התוצאות מהשאילתה הישנה ומפעילות את החיפוש החדש באופן מיידי.
סינון נתונים
פעולה נפוצה נוספת היא סינון. אפשר לסנן נתונים על סמך קריטריונים של המשתמש, או להסיר נתונים מממשק המשתמש אם צריך להסתיר אותם על סמך קריטריונים אחרים.
צריך למקם את פעולות הסינון האלה בתוך הקריאה ל-map()
כי המסנן חל על האובייקט PagingData
. אחרי שהנתונים מסוננים מ-PagingData
, המכונה החדשה של PagingData
מועברת לשכבת ממשק המשתמש להצגה.
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
.
יש שלושה שלבים להוספת מפרידים לרשימה שמחולקת לדפים:
- ממירים את מודל ממשק המשתמש כך שיכלול את הפריטים המפרידים.
- טרנספורמציה של מקור הנתונים כדי להוסיף באופן דינמי את התוספי ההפרדה בין טעינת הנתונים לבין הצגת הנתונים.
- עדכון ממשק המשתמש כדי לטפל בפריטי מפריד.
המרת מודל ממשק המשתמש
ספריית הגיליון מוסיפה את הפריטים שמפרידים בין הרשימות ל-RecyclerView
כפריטי רשימה אמיתיים, אבל צריך להיות ניתן להבדיל בין הפריטים שמפרידים לבין פריטי הנתונים ברשימה כדי לאפשר להם להתחבר לסוג אחר של ViewHolder
עם ממשק משתמש ייחודי. הפתרון הוא ליצור כיתה אטומה ב-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; } }); });
טיפול בפסי הפרדה בממשק המשתמש
השלב האחרון הוא לשנות את ממשק המשתמש כך שיתאים לסוג הפריט המפריד.
יוצרים פריסה ומחזיק תצוגה לפריטי המפריד, ומשנים את מתאם הרשימה כך שישתמש ב-RecyclerView.ViewHolder
כסוג של מחזיק התצוגה, כדי שיוכל לטפל ביותר מסוג אחד של מחזיק תצוגה. לחלופין, אפשר להגדיר קלאס בסיס משותף שכל הכיתות של מחזיק התצוגה של הפריט ושל מחזיק התצוגה של התו המפריד יופיעו בו.
צריך גם לבצע את השינויים הבאים במתאם הרשימה:
- מוסיפים מקרים לשיטות
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); } }
הימנעות מעבודה כפולה
אחת מהבעיות העיקריות שצריך להימנע מהן היא לגרום לאפליקציה לבצע עבודה מיותרת. אחזור נתונים הוא פעולה יקרה, וגם טרנספורמציות של נתונים יכולות לגזול זמן יקר. אחרי שהנתונים נטענים ומוכנים להצגה בממשק המשתמש, צריך לשמור אותם למקרה שיהיה שינוי בתצורה ויהיה צורך ליצור מחדש את ממשק המשתמש.
הפעולה 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 מושבת
- טעינת נתונים שמחולקים לדפים והצגתם
- בדיקת ההטמעה של דפים
- ניהול מצבי הטעינה והצגתם