כשעובדים עם נתונים שמחולקים לדפים, לעיתים קרובות צריך לשנות את זרם הנתונים בזמן הטעינה שלו. לדוגמה, יכול להיות שתצטרכו לסנן רשימה של פריטים או להמיר פריטים לסוג אחר לפני שתציגו אותם בממשק המשתמש. תרחיש נפוץ נוסף לשימוש בהמרת מקור נתונים הוא הוספת מפרידים לרשימה.
באופן כללי יותר, החלת טרנספורמציות ישירות על מקור הנתונים מאפשרת לכם להפריד בין מבני המאגר לבין מבני ממשק המשתמש.
בדף הזה אנחנו מניחים שאתם מכירים את השימוש הבסיסי בספריית Paging.
החלת טרנספורמציות בסיסיות
מכיוון ש-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. }
המרת נתונים
הפעולה הבסיסית ביותר בזרם נתונים היא המרה שלו לסוג אחר. אחרי שיש לכם גישה לאובייקט PagingData, אתם יכולים לבצע פעולה map() על כל פריט בודד ברשימה עם החלוקה לדפים באובייקט PagingData.
תרחיש שימוש נפוץ הוא מיפוי של אובייקט בשכבת רשת או מסד נתונים לאובייקט שמשמש ספציפית בשכבת ממשק המשתמש. בדוגמה הבאה אפשר לראות איך מבצעים מיפוי מסוג כזה:
pager.flow // Type is Flow<PagingData<User>>. .map { pagingData -> pagingData.map { user -> UiModel(user) } }
המרת נתונים נפוצה נוספת היא קבלת קלט מהמשתמש, כמו מחרוזת שאילתה, והמרתו לפלט של הבקשה שיוצג. כדי להגדיר את זה, צריך להאזין לקלט השאילתה של המשתמש ולתעד אותו, לבצע את הבקשה ולדחוף את תוצאת השאילתה בחזרה לממשק המשתמש.
אפשר להאזין לקלט של השאילתה באמצעות API של סטרימינג. שומרים את ההפניה לשידור ב-ViewModel. לשכבת ממשק המשתמש לא צריכה להיות גישה ישירה אליו. במקום זאת, צריך להגדיר פונקציה כדי להודיע ל-ViewModel על השאילתה של המשתמש.
private val queryFlow = MutableStateFlow("") fun onQueryChanged(query: String) { queryFlow.value = query }
כשערך השאילתה משתנה במקור הנתונים, אפשר לבצע פעולות כדי להמיר את ערך השאילתה לסוג הנתונים הרצוי ולהחזיר את התוצאה לשכבת ממשק המשתמש. פונקציית ההמרה הספציפית תלויה בשפה ובמסגרת שבהן נעשה שימוש, אבל כולן מספקות פונקציונליות דומה.
val querySearchResults: Flow<User> = queryFlow.flatMapLatest { query -> // The database query returns a Flow which is output through // querySearchResults userDatabase.searchBy(query) }
שימוש בפעולות כמו flatMapLatest או switchMap מבטיח שרק התוצאות העדכניות ביותר יוחזרו לממשק המשתמש. אם המשתמש משנה את קלט השאילתה לפני שהפעולה במסד הנתונים מסתיימת, הפעולות האלה מבטלות את התוצאות מהשאילתה הישנה ומפעילות את החיפוש החדש באופן מיידי.
סינון נתונים
פעולה נפוצה נוספת היא סינון. אפשר לסנן נתונים לפי קריטריונים של המשתמש, או להסיר נתונים מממשק המשתמש אם צריך להסתיר אותם לפי קריטריונים אחרים.
צריך למקם את פעולות המסנן האלה בתוך הקריאה map() כי המסנן חל על האובייקט PagingData. אחרי שהנתונים מסוננים מ-PagingData, מועבר מופע חדש של PagingData לשכבת ממשק המשתמש לתצוגה.
pager.flow // Type is Flow<PagingData<User>>. .map { pagingData -> pagingData.filter { user -> !user.hiddenFromUi } }
הוספת מפרידים לרשימה
ספריית ה-Paging תומכת במפרידים דינמיים ברשימה. כדי לשפר את הקריאות של הרשימה, אפשר להוסיף מפרידים ישירות למקור הנתונים כרכיבים שניתנים להרכבה בפריסה. כתוצאה מכך, רכיבי Separator הם רכיבים הניתנים להרכבה עם כל התכונות, שמאפשרים אינטראקטיביות מלאה, סגנון וסמנטיקה של נגישות.
יש שלושה שלבים להוספת מפרידים לרשימה עם מספור עמודים:
- ממירים את מודל ממשק המשתמש כדי להתאים אותו לפריטי ההפרדה. אחת הדרכים לעשות זאת היא להוסיף את פריט הנתונים והמפריד למחלקה אטומה אחת. כך ממשק המשתמש יכול לטפל בכמה סוגי פריטים באותה רשימה.
- להפוך את מקור הנתונים כדי להוסיף באופן דינמי את התווים המפרידים בין טעינת הנתונים לבין הצגת הנתונים.
- עדכון ממשק המשתמש כדי לטפל בפריטים של מפרידים.
המרת מודל ממשק המשתמש
ספריית ה-Paging מוסיפה מפרידי רשימות לממשק המשתמש כפריטים אמיתיים ברשימה, אבל פריטי המפריד צריכים להיות שונים מפריטי הנתונים ברשימה כדי לוודא ששני הסוגים של הרכיבים ניתנים להרכבה ומוצגים בצורה ברורה. הפתרון הוא ליצור מחלקת Kotlin sealed עם מחלקות משנה שמייצגות את הנתונים ואת התו המפריד. לחלופין, אפשר ליצור מחלקה בסיסית שמוכללת במחלקה של פריט הרשימה ובמחלקה של התו המפריד.
נניח שרוצים להוסיף מפרידים לרשימה עם דפדוף של 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() }
שינוי מקור הנתונים
צריך להחיל טרנספורמציות על מקור הנתונים אחרי הטעינה ולפני ההצגה. ההמרות צריכות לבצע את הפעולות הבאות:
- ממירים את הפריטים ברשימה שנטענה כך שישקפו את סוג פריט הבסיס החדש.
- משתמשים בשיטה
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 } } }
הגדרת מפרידים בממשק המשתמש
השלב האחרון הוא לשנות את ממשק המשתמש כך שיתאים לסוג הפריט של המפריד.
בפריסה עצלה, אפשר לטפל בכמה סוגי פריטים על ידי בדיקת הסוג של כל UiModel. כשחוזרים על הפעולה על הנתונים שמוצגים בדפים, משתמשים בהצהרה whenכדי לקרוא לפונקציה המתאימה. כך תוכלו לספק ממשק משתמש ייחודי לפריטי נתונים ולמפרידים.
@Composable fun UserList(pagingItems: LazyPagingItems) { LazyColumn { items( count = pagingItems.itemCount, key = { index -> val item = pagingItems.peek(index) when (item) { is UiModel.UserModel -> item.user.id is UiModel.SeparatorModel -> item.description else -> index } } ) { index -> when (val item = pagingItems[index]) { is UiModel.UserModel -> UserItemComposable(item.user) is UiModel.SeparatorModel -> SeparatorComposable(item.description) null -> PlaceholderComposable() } } } }
איך להימנע מכפילויות בעבודה
בעיה מרכזית שכדאי להימנע ממנה היא שהאפליקציה מבצעת עבודה מיותרת. שליפת נתונים היא פעולה יקרה, וגם טרנספורמציות של נתונים יכולות לגזול זמן יקר. אחרי שהנתונים נטענים ומוכנים להצגה בממשק המשתמש, צריך לשמור אותם למקרה שיתרחש שינוי בהגדרות ויהיה צורך ליצור מחדש את ממשק המשתמש.
הפעולה cachedIn() שומרת במטמון את התוצאות של כל ההמרות שמתבצעות לפני הפעולה. בדרך כלל, מפעילים את האופרטור הזה ב-ViewModel לפני שמציגים את Flow לרכיבים הניתנים להרכבה.
כדי לנהל את המטמון בצורה נכונה, צריך להעביר את CoroutineScope אל cachedIn(), כמו בדוגמה הבאה שבה נעשה שימוש ב-viewModelScope.
pager.flow // Type is Flow<PagingData<User>>. .map { pagingData -> pagingData.filter { user -> !user.hiddenFromUi } .map { user -> UiModel.UserModel(user) } } .cachedIn(viewModelScope)
מידע נוסף על שימוש ב-cachedIn() עם מקור נתונים של PagingData
מקורות מידע נוספים
איפה אפשר למצוא מידע נוסף על ספריית Paging?
מסמכים
צפייה בתוכן
מומלץ בשבילכם
- הערה: טקסט הקישור מוצג כש-JavaScript מושבת
- טעינה והצגה של נתונים עם חלוקה לדפים
- בדיקת ההטמעה של חלוקה לדפים
- ניהול והצגה של מצבי טעינה