תפקיד ממשק המשתמש הוא להציג את נתוני האפליקציה על המסך. ממשק המשתמש משמש גם כנקודת האינטראקציה העיקרית של המשתמשים. בכל פעם שהנתונים משתנים, בגלל אינטראקציה של משתמש (למשל לחיצה על לחצן) או קלט חיצוני (למשל תגובה של רשת), ממשק המשתמש מתעדכן בהתאם לשינויים האלה. בפועל, ממשק המשתמש הוא ייצוג חזותי של מצב האפליקציה כפי שהוא מאוחזר משכבת הנתונים.
עם זאת, נתוני האפליקציה שמתקבלים משכבת הנתונים הם בדרך כלל בפורמט שונה מהמידע שצריך להציג. לדוגמה, יכול להיות שתצטרכו רק חלק מהנתונים בשביל ממשק המשתמש, או שתצטרכו למזג שני מקורות נתונים שונים כדי להציג מידע שרלוונטי למשתמש. לא משנה איזו לוגיקה תיישמו, תצטרכו להעביר לממשק המשתמש את כל המידע שהוא צריך כדי להציג את עצמו באופן מלא. שכבת ממשק המשתמש היא צינור הנתונים שממיר שינויים בנתוני האפליקציה לפורמט שניתן להצגה בממשק המשתמש, ואז מציג אותם.
מקרה לדוגמה בסיסי
נניח שיש אפליקציה שמביאה למשתמש כתבות לקריאה. באפליקציה יש מסך מאמרים שבו מוצגים מאמרים שזמינים לקריאה, והיא גם מאפשרת למשתמשים מחוברים להוסיף לסימנייה מאמרים שבאמת בולטים. יכול להיות שיש הרבה מאמרים בכל זמן נתון, ולכן הקוראים צריכים להיות מסוגלים לעיין במאמרים לפי קטגוריה. לסיכום, האפליקציה מאפשרת למשתמשים:
- צפייה במאמרים שזמינים לקריאה.
- לעיין במאמרים לפי קטגוריה.
- להיכנס לחשבון ולהוסיף כתבות מסוימות לסימניות.
- גישה לחלק מתכונות הפרימיום אם אתם עומדים בדרישות.
בקטעים הבאים נשתמש בדוגמה הזו כבמקרה לדוגמה כדי להציג את העקרונות של זרימת נתונים חד-כיוונית, וגם כדי להמחיש את הבעיות שהעקרונות האלה עוזרים לפתור בהקשר של ארכיטקטורת אפליקציות בשכבת ממשק המשתמש.
ארכיטקטורה של שכבת ממשק המשתמש
המונח ממשק משתמש מתייחס לרכיבים בממשק המשתמש, כמו קונטיינרים ופונקציות קומפוזביליות שמציגות נתונים. Jetpack Compose היא ערכת הכלים המומלצת לבניית ממשקי משתמש ל-Android. מכיוון שהתפקיד של שכבת הנתונים הוא להחזיק את נתוני האפליקציה, לנהל אותם ולספק גישה אליהם, שכבת ממשק המשתמש צריכה לבצע את השלבים הבאים:
- צריכת נתוני אפליקציות והמרתם לנתונים שקל לעבד בממשק המשתמש.
- לקבל נתונים שאפשר להציג בממשק המשתמש ולהפוך אותם לרכיבים בממשק המשתמש כדי להציג אותם למשתמש.
- להשתמש באירועי קלט של משתמשים מרכיבי ממשק המשתמש האלה ולשקף את ההשפעות שלהם בנתוני ממשק המשתמש לפי הצורך.
- חוזרים על שלבים 1 עד 3 לפי הצורך.
בהמשך המדריך הזה נסביר איך להטמיע שכבת ממשק משתמש שמבצעת את השלבים האלה. באופן ספציפי, במדריך הזה מוסבר על המשימות והמושגים הבאים:
- איך מגדירים את מצב ממשק המשתמש
- זרימת נתונים חד-כיוונית (UDF) כאמצעי ליצירה ולניהול של מצב ממשק המשתמש
- איך חושפים את מצב ממשק המשתמש באמצעות סוגי נתונים שניתנים לצפייה בהתאם לעקרונות של UDF
- איך מטמיעים ממשק משתמש שמשתמש במצב ממשק המשתמש שניתן לצפייה
ההגדרה הבסיסית ביותר היא של מצב ממשק המשתמש.
הגדרת מצב ממשק המשתמש
במחקר המקרה שצוין קודם, בממשק המשתמש מוצגת רשימה של מאמרים עם מטא-נתונים של כל מאמר. המידע הזה שהאפליקציה מציגה למשתמש הוא מצב ממשק המשתמש.
במילים אחרות, אם ממשק המשתמש הוא מה שהמשתמש רואה, מצב ממשק המשתמש הוא מה שהאפליקציה אומרת שהמשתמש צריך לראות. בדומה לשני צדדים של אותו מטבע, ממשק המשתמש הוא הייצוג החזותי של מצב ממשק המשתמש. כל שינוי במצב ממשק המשתמש משתקף בו באופן מיידי.
לדוגמה, כדי לעמוד בדרישות של אפליקציית החדשות, אפשר להגדיר את מחלקת הנתונים NewsUiState הבאה, שתכיל את המידע שנדרש כדי להציג את ממשק המשתמש באופן מלא:
data class NewsUiState( val isSignedIn: Boolean = false, val isPremium: Boolean = false, val newsItems: List<NewsItemUiState> = listOf(), val userMessages: List<Message> = listOf() ) data class NewsItemUiState( val title: String, val body: String, val bookmarked: Boolean = false, // ... )
מידע נוסף על מצב ממשק המשתמש זמין במאמר מצב ו-Jetpack Compose.
אי-שינוי
ההגדרה של מצב ממשק המשתמש בדוגמה הקודמת היא בלתי ניתנת לשינוי. היתרון העיקרי של זה הוא שאובייקטים שלא ניתן לשנות מספקים ערבויות לגבי מצב האפליקציה בנקודת זמן מסוימת. כך ממשק המשתמש יכול להתמקד בתפקיד העיקרי שלו: לקרוא את המצב ולעדכן את רכיבי ממשק המשתמש בהתאם. לעולם אל תשנו את מצב ממשק המשתמש ישירות בממשק המשתמש, אלא אם ממשק המשתמש עצמו הוא המקור היחיד של הנתונים שלו. הפרה של העיקרון הזה מובילה לכך שיש כמה מקורות אמת לאותו פריט מידע, וכתוצאה מכך לנתונים לא עקביים ולבאגים לא בולטים.
לדוגמה, נניח שאתם רוצים להשתמש במקרה לדוגמה שצוין קודם.
אם הדגל bookmarked באובייקט NewsItemUiState ממצב ממשק המשתמש מתעדכן במחלקה Activity, הדגל הזה מתחרה בשכבת הנתונים כמקור של סטטוס הסימון של מאמר. מחלקות נתונים שלא ניתן לשנות הן שימושיות מאוד למניעת חוסר עקביות מסוג זה.
מוסכמות מתן השמות במדריך הזה
במדריך הזה, שמות המחלקות של מצב ממשק המשתמש מבוססים על הפונקציונליות של המסך או של חלק מהמסך שהן מתארות. המוסכמה היא כזו:
functionality + UiState.
לדוגמה, מצב של מסך שבו מוצגות חדשות יכול להיקרא NewsUiState, ומצב של פריט חדשות ברשימה של פריטי חדשות יכול להיות NewsItemUiState.
ניהול מצב באמצעות זרימת נתונים חד-כיוונית
בקטע הקודם ראינו שמצב ממשק המשתמש הוא תמונת מצב שלא ניתנת לשינוי של הפרטים שדרושים לעיבוד ממשק המשתמש. עם זאת, הנתונים באפליקציות הם דינמיים, ולכן המצב יכול להשתנות עם הזמן. יכול להיות שהסיבה לכך היא אינטראקציה של משתמשים או אירועים אחרים שמשנים את נתוני הבסיס שמשמשים לאכלוס האפליקציה.
כדי לעבד את האינטראקציות האלה, כדאי להשתמש במתווך שיגדיר את הלוגיקה שתחול על כל אירוע ויבצע טרנספורמציה של מקורות הנתונים הבסיסיים כדי ליצור מצב של ממשק המשתמש. אפשר לשכן את האינטראקציות האלה ואת הלוגיקה שלהן בממשק המשתמש עצמו, אבל זה עלול להפוך במהירות למסורבל כי ממשק המשתמש ייקח על עצמו יותר מדי אחריות. בנוסף, זה יכול להשפיע על יכולת הבדיקה כי הקוד שמתקבל מצומד מאוד. אלא אם מצב ממשק המשתמש פשוט מאוד, צריך לוודא שהאחריות היחידה של ממשק המשתמש היא לצרוך ולהציג את מצב ממשק המשתמש.
בקטע הזה נדון בתבנית ארכיטקטורה שנקראת זרימת נתונים חד-כיוונית (UDF), שעוזרת להבטיח את ההפרדה התקינה הזו של האחריות.
מאחסני מצב
מחזיקי מצב הם המחלקות שאחראיות על יצירת מצב ממשק המשתמש ועל הלוגיקה שנדרשת ליצירת המצב הזה. יש מאגרי מצב בגדלים שונים, בהתאם להיקף של רכיבי ממשק המשתמש התואמים שהם מנהלים. הם יכולים לנהל ווידג'ט יחיד כמו סרגל אפליקציות תחתון, או מסך שלם או יעד ניווט.
במקרה השני, ההטמעה האופיינית היא מופע של ViewModel, אבל יכול להיות שמחלקה פשוטה תספיק, בהתאם לדרישות של האפליקציה. לדוגמה, אפליקציית החדשות ממחקר המקרה משתמשת במחלקה NewsViewModel בתור מחזיק מצב כדי ליצור את מצב ממשק המשתמש של המסך שמוצג בקטע הזה.
יש הרבה דרכים ליצור מודל של התלות ההדדית בין ממשק המשתמש לבין רכיב ייצור המצב שלו. עם זאת, מכיוון שאפשר להבין את האינטראקציה בין ממשק המשתמש לבין המחלקה ViewModel
שלו בעיקר כקלט של אירוע ופלט של המצב שנובע ממנו,
אפשר לייצג את הקשר ביניהם כמו בתרשים הבא:
הדפוס שבו המצב זורם למטה והאירועים זורמים למעלה נקרא זרימת נתונים חד-כיוונית (UDF). ההשלכות של הדפוס הזה על ארכיטקטורת האפליקציה הן:
- ה-ViewModel מחזיק את ה-State וחושף אותו לשימוש בממשק המשתמש. מצב ממשק המשתמש הוא נתונים של האפליקציה שעברו טרנספורמציה על ידי ViewModel.
- ממשק המשתמש מודיע ל-ViewModel על אירועים של משתמשים.
- ה-ViewModel מטפל בפעולות המשתמש ומעדכן את המצב.
- המצב המעודכן מועבר בחזרה לממשק המשתמש לצורך עיבוד.
- הפעולה שלמעלה חוזרת על עצמה לגבי כל אירוע שגורם לשינוי במצב.
במקרה של יעדי ניווט או מסכים, ה-ViewModel פועל עם מאגרי מידע או עם מחלקות של תרחישי שימוש כדי לקבל נתונים ולהפוך אותם למצב של ממשק המשתמש, תוך שילוב של השפעות האירועים שעשויים לגרום לשינויים במצב. מחקר המקרה שצוין קודם מכיל רשימה של מאמרים, שלכל אחד מהם יש כותרת, תיאור, מקור, שם מחבר, תאריך הוצאה לאור וסימון אם הוא נוסף לסימנייה. ממשק המשתמש של כל פריט כתבה נראה כך:
דוגמה לאירוע שיכול לגרום לשינויים במצב היא משתמש שמבקש להוסיף מאמר לסימניות. בתור יצרן המצב, ה-ViewModel אחראי להגדיר את כל הלוגיקה שנדרשת כדי לאכלס את כל השדות במצב ממשק המשתמש ולעבד את האירועים שנדרשים כדי שממשק המשתמש יוצג באופן מלא.
בקטעים הבאים נבחן מקרוב את האירועים שגורמים לשינויים במצב ואת האופן שבו אפשר לעבד אותם באמצעות UDF.
סוגים של לוגיקה
הוספת מאמר לסימנייה היא דוגמה ללוגיקה עסקית כי היא מוסיפה ערך לאפליקציה. מידע נוסף בנושא זמין בדף שכבת הנתונים. עם זאת, יש סוגים שונים של לוגיקה שחשוב להגדיר:
- לוגיקה עסקית היא הטמעה של דרישות המוצר לגבי נתוני האפליקציה. כמו שכבר ציינו, דוגמה אחת היא הוספת מאמר למועדפים באפליקציה של מחקר המקרה. לוגיקה עסקית בדרך כלל ממוקמת בשכבות של הדומיין או הנתונים, אבל אף פעם לא בשכבת ממשק המשתמש.
- הלוגיקה של התנהגות ממשק המשתמש או הלוגיקה של ממשק המשתמש היא האופן שבו מוצגים שינויים במצב במסך. לדוגמה, קבלת הטקסט הנכון להצגה במסך באמצעות Android
Resources, מעבר למסך מסוים כשהמשתמש לוחץ על לחצן או הצגת הודעת משתמש במסך באמצעות toast או snackbar.
חשוב לשמור את הלוגיקה של ממשק המשתמש בממשק המשתמש ולא ב-ViewModel, במיוחד כשמדובר בסוגים של ממשק משתמש כמו Context.
אם ממשק המשתמש הופך למורכב מדי ואתם רוצים להעביר את הלוגיקה של ממשק המשתמש למחלקה אחרת כדי להקל על בדיקות ולהפריד בין נושאים, אתם יכולים ליצור מחלקה פשוטה בתור מחזיק מצב. מחלקות פשוטות שנוצרו בממשק המשתמש יכולות להסתמך על Android SDK כי הן פועלות לפי מחזור החיים של ממשק המשתמש. לאובייקטים של ViewModel יש מחזור חיים ארוך יותר.
מידע נוסף על מחזיקי מצב ועל האופן שבו הם משתלבים בהקשר של עזרה בבניית ממשק משתמש זמין במאמר מדריך Jetpack Compose State.
למה כדאי להשתמש בפונקציות מוגדרות על ידי המשתמש?
מודל ה-UDF מתאר את מחזור הייצור של מצב, כמו שמוצג באיור 4. הוא גם מפריד בין המקום שבו מתחילים שינויים במצב, המקום שבו הם עוברים שינוי והמקום שבו הם נצרכים בסופו של דבר. ההפרדה הזו מאפשרת לממשק המשתמש לעשות בדיוק את מה שהשם שלו מרמז: להציג מידע על ידי מעקב אחרי שינויים במצב, ולהעביר את השינויים האלה אל ViewModel כדי לשקף את כוונת המשתמש.
במילים אחרות, UDF מאפשר את הפעולות הבאות:
- עקביות הנתונים. יש מקור מהימן אחד לממשק המשתמש.
- יכולת בדיקה. מקור המצב מבודד ולכן ניתן לבדיקה ללא תלות בממשק המשתמש.
- יכולת תחזוקה. השינוי במצב פועל לפי דפוס מוגדר היטב, שבו השינויים הם תוצאה של אירועים שקשורים למשתמשים ושל מקורות הנתונים שהם שולפים מהם.
חשיפת מצב ממשק המשתמש
אחרי שמגדירים את מצב ממשק המשתמש ומחליטים איך לנהל את הייצור של המצב הזה, השלב הבא הוא להציג את המצב שנוצר בממשק המשתמש.
כשמשתמשים ב-UDF כדי לנהל את יצירת המצב, אפשר להתייחס למצב שנוצר כאל זרם – במילים אחרות, נוצרות לאורך זמן כמה גרסאות של המצב. חשיפת מצב ממשק המשתמש במאגר נתונים ניתן לצפייה כמו StateFlow. כך ממשק המשתמש יכול להגיב לכל שינוי במצב בלי שתצטרכו לשלוף נתונים ישירות מ-ViewModel באופן ידני. בנוסף, היתרון של הגישה הזו הוא שתמיד יש במטמון את הגרסה העדכנית של מצב ממשק המשתמש, וזה שימושי לשחזור מהיר של המצב אחרי שינויים בהגדרות.
class NewsViewModel( // ... ) : ViewModel() { val uiState: NewsUiState = /* ... */ }
למבוא ל-Kotlin flows, אפשר לעיין במאמר Kotlin flows on Android.
כדי ללמוד איך להשתמש ב-StateFlow כמאגר נתונים גלויים, אפשר לעיין בשיעור Codelab בנושא מצב מתקדם ותופעות לוואי ב-Jetpack Compose.
במקרים שבהם הנתונים שמוצגים בממשק המשתמש הם יחסית פשוטים, כדאי לעטוף את הנתונים בסוג של מצב ממשק משתמש, כי הוא מעביר את הקשר בין הפליטה של מחזיק המצב לבין המסך או רכיב ממשק המשתמש שמשויכים אליו. ככל שרכיב ממשק המשתמש נעשה מורכב יותר, קל להוסיף אותו להגדרה של מצב ממשק המשתמש, כך שאפשר להתאים את המידע הנוסף שנדרש כדי לעבד את רכיב ממשק המשתמש.
דרך נפוצה ליצור סטרימינג של UiState היא לחשוף מאפיין mutableStateOf עם private set, ולשמור את המצב כניתן לשינוי ב-ViewModel אבל לקריאה בלבד בממשק המשתמש.
class NewsViewModel( // ... ) : ViewModel() { var uiState by mutableStateOf(NewsUiState()) private set // ... }
לאחר מכן, ה-ViewModel יכול לחשוף שיטות שמשנות את המצב באופן פנימי, ולפרסם עדכונים לשימוש בממשק המשתמש. לדוגמה, אם אתם צריכים לבצע פעולה אסינכרונית.
אפשר להפעיל קורוטינה באמצעות viewModelScope ואז לעדכן את המצב הניתן לשינוי אחרי ההשלמה.
class NewsViewModel( private val repository: NewsRepository, // ... ) : ViewModel() { var uiState by mutableStateOf(NewsUiState()) private set private var fetchJob: Job? = null fun fetchArticles(category: String) { fetchJob?.cancel() fetchJob = viewModelScope.launch { try { val newsItems = repository.newsItemsForCategory(category) uiState = uiState.copy(newsItems = newsItems) } catch (ioe: IOException) { // Handle the error and notify the UI when appropriate. val messages = getMessagesFromThrowable(ioe) uiState = uiState.copy(userMessages = messages) } } } }
בדוגמה הקודמת, המחלקה NewsViewModel מנסה לאחזר מאמרים לקטגוריה מסוימת, ואז משקפת את תוצאת הניסיון – הצלחה או כישלון – במצב ממשק המשתמש, שבו ממשק המשתמש יכול להגיב בהתאם.
מידע נוסף על טיפול בשגיאות זמין בקטע הצגת שגיאות על המסך.
שיקולים נוספים
בנוסף להנחיות הקודמות, כדאי לקחת בחשבון את הנקודות הבאות כשחושפים את מצב ממשק המשתמש:
משתמשים באובייקט יחיד של מצב ממשק המשתמש כדי לטפל במצבים שקשורים זה לזה. כך יש פחות חוסר עקביות וקל יותר להבין את הקוד. אם תציגו את רשימת פריטי החדשות ואת מספר הסימניות בשני פידים שונים, יכול להיות שאחד מהם יעודכן והשני לא. כשמשתמשים בזרם אחד, שני הרכיבים מתעדכנים. בנוסף, יכול להיות שחלק מהלוגיקה העסקית ידרוש שילוב של מקורות. לדוגמה, יכול להיות שתרצו להציג לחצן לסימון דף רק אם המשתמש מחובר וגם אם הוא מנוי לשירות חדשות בתשלום. אפשר להגדיר מחלקה של מצב ממשק המשתמש באופן הבא:
data class NewsUiState( val isSignedIn: Boolean = false, val isPremium: Boolean = false, val newsItems: List<NewsItemUiState> = listOf() ) val NewsUiState.canBookmarkNews: Boolean get() = isSignedIn && isPremium
בהצהרה הזו, הנראות של לחצן הסימנייה היא מאפיין נגזר של שני מאפיינים אחרים. ככל שהלוגיקה העסקית נעשית מורכבת יותר, כך חשוב יותר שיהיה מחלקה אחת
UiStateשבה כל המאפיינים זמינים באופן מיידי.מצבי ממשק המשתמש: שידור אחד או כמה שידורים? העיקרון המנחה לבחירה בין חשיפת מצב ממשק המשתמש בזרם אחד או בכמה זרמים הוא הקשר בין הפריטים שמועברים. היתרונות העיקריים של חשיפה לזרם יחיד הם נוחות ועקביות של הנתונים: הצרכנים של המצב תמיד מקבלים את המידע העדכני ביותר שזמין בכל רגע נתון. עם זאת, יש מקרים שבהם כדאי להשתמש בזרמים נפרדים של מצב מ-ViewModel:
סוגי נתונים לא קשורים: יכול להיות שחלק מהמצבים שנדרשים לעיבוד ממשק המשתמש יהיו בלתי תלויים לחלוטין זה בזה. במקרים כאלה, העלויות של איגוד המדינות השונות האלה עשויות להיות גבוהות מהיתרונות, במיוחד אם אחת מהמדינות האלה מתעדכנת בתדירות גבוהה יותר מהאחרת.
UiStateהשוואה: ככל שיש יותר שדות באובייקטUiState, כך גדל הסיכוי שהסטרים ישלח כתוצאה מעדכון של אחד מהשדות שלו. לרכיבי ממשק המשתמש אין מנגנון השוואה כדי להבין אם פליטות עוקבות הן שונות או זהות, ולכן כל פליטה גורמת לעדכון של רכיב ממשק המשתמש. המשמעות היא שאולי יהיה צורך בשימוש ב-methods שלFlowAPI כמוdistinctUntilChanged()כדי לצמצם את הסיכון.
מידע נוסף על רינדור ומצב ממשק המשתמש זמין במאמר מחזור החיים של פונקציות Composable.
שימוש במצב ממשק המשתמש
כדי לצרוך את הזרם של אובייקטים מסוג UiState בממשק המשתמש, צריך להשתמש באופרטור של הטרמינל עבור סוג הנתונים הגלויים שבו אתם משתמשים. לדוגמה,
ב-Kotlin flows משתמשים בשיטה collect() או בווריאציות שלה.
כשמשתמשים בממשק המשתמש כדי לצרוך נתונים שניתנים לצפייה, חשוב לקחת בחשבון את מחזור החיים של ממשק המשתמש. לא מאפשרים לממשק המשתמש לעקוב אחרי מצב ממשק המשתמש כשהקומפוזיציה לא מוצגת למשתמש. מידע נוסף על הנושא הזה זמין בפוסט הזה בבלוג. כשמשתמשים ב-Flows, מומלץ לטפל בבעיות שקשורות למחזור החיים באמצעות היקף הקורוטינה המתאים ו-collectAsStateWithLifecycle API:
@Composable private fun ConversationScreen( conversationViewModel: ConversationViewModel = viewModel() ) { val messages by conversationViewModel.messages.collectAsStateWithLifecycle() ConversationScreen( messages = messages, onSendMessage = { message: Message -> conversationViewModel.sendMessage(message) } ) } @Composable private fun ConversationScreen( messages: List<Message>, onSendMessage: (Message) -> Unit ) { MessagesList(messages, onSendMessage) /* ... */ }
הצגת פעולות בתהליך
דרך פשוטה לייצג מצבי טעינה במחלקה UiState היא באמצעות שדה בוליאני:
data class NewsUiState( val isFetchingArticles: Boolean = false, // ... )
הערך של הדגל הזה מייצג את הנוכחות או היעדר של סרגל התקדמות בממשק המשתמש.
@Composable fun LatestNewsScreen( modifier: Modifier = Modifier, viewModel: NewsViewModel = viewModel() ) { Box(modifier.fillMaxSize()) { if (viewModel.uiState.isFetchingArticles) { CircularProgressIndicator(Modifier.align(Alignment.Center)) } // Add other UI elements. For example, the list. } }
הצגת שגיאות במסך
הצגת שגיאות בממשק המשתמש דומה להצגת פעולות בתהליך, כי אפשר לייצג את שתיהן בקלות באמצעות ערכים בוליאניים שמציינים את הנוכחות או ההיעדר שלהן. עם זאת, יכול להיות שהשגיאות יכללו גם הודעה נלווית שמועברת בחזרה למשתמש, או פעולה שמשויכת אליהן ומנסה שוב את הפעולה שנכשלה. לכן, בזמן שטעינה של פעולה מתבצעת או לא מתבצעת, יכול להיות שיהיה צורך ליצור מודל של מצבי שגיאה באמצעות מחלקות נתונים שמכילות את המטא-נתונים שמתאימים להקשר של השגיאה.
נחזור לדוגמה הקודמת שבה מוצג סרגל התקדמות בזמן שליפת מאמרים. אם הפעולה הזו גורמת לשגיאה, כדאי להציג למשתמש הודעה אחת או יותר עם פרטים על מה שהשתבש.
data class Message(val id: Long, val message: String) data class NewsUiState( val userMessages: List<Message> = listOf(), // ... )
אחר כך אפשר להציג את הודעות השגיאה למשתמש בצורה של רכיבי ממשק משתמש כמו סנאקברים. מידע נוסף על האופן שבו אירועי ממשק משתמש נוצרים ונצרכים זמין במאמר בנושא אירועי ממשק משתמש.
שרשור ובו-זמניות
חשוב לוודא שכל הפעולות שמתבצעות ב-ViewModel הן main-safe – כלומר, אפשר להפעיל אותן מה-thread הראשי. שכבות הנתונים והדומיין אחראיות להעברת עבודה לשרשור אחר.
אם ViewModel מבצע פעולות ארוכות טווח, הוא גם אחראי להעברת הלוגיקה הזו לשרשור ברקע. שגרות המשנה של Kotlin הן דרך מצוינת לנהל פעולות מקבילות, ורכיבי Jetpack Architecture מספקים תמיכה מובנית בהן. מידע נוסף על שימוש בשגרות משנה באפליקציות ל-Android
ניווט
שינויים בניווט באפליקציה נובעים לרוב מפליטות דמויות אירועים. לדוגמה,
אחרי שכיתה SignInViewModel מבצעת כניסה, יכול להיות של-UiState יש שדה isSignedIn שמוגדר ל-true. השימוש בטריגרים כאלה דומה לשימוש בטריגרים שמתואר בקטע הקודם בנושא Consume UI state, אבל ההטמעה של השימוש מועברת אל Navigation component.
מידע נוסף על ניווט בממשק המשתמש זמין במאמר ניווט 3.
חלוקה לדפים
השימוש ב-Paging library מתבצע בממשק המשתמש עם סוג שנקרא PagingData. מכיוון ש-PagingData מייצג ומכיל פריטים שיכולים להשתנות לאורך זמן – במילים אחרות, זה לא סוג שלא ניתן לשינוי – אל תייצגו אותו במצב ממשק משתמש שלא ניתן לשינוי.
במקום זאת, חושפים אותו מ-ViewModel באופן עצמאי בזרם משלו.
בדוגמה הבאה מוצג Compose API של ספריית Paging:
@Composable fun MyScreen(flow: Flow<PagingData<String>>) { val lazyPagingItems = flow.collectAsLazyPagingItems() LazyColumn { items( lazyPagingItems.itemCount, key = lazyPagingItems.itemKey { it } ) { index -> val item = lazyPagingItems[index] Text("Item is $item") } } }
אנימציות
כדי לספק מעברים חלקים בניווט ברמה העליונה, כדאי להמתין עד שהנתונים ייטענו במסך השני לפני שמתחילים את האנימציה.
מידע נוסף על מעברים בניווט זמין במאמרים Navigation 3 ו-Shared element transitions in Compose.
מקורות מידע נוספים
צפייה בתוכן
דוגמאות
בדוגמאות הבאות של Google מוצג השימוש בשכבת ממשק המשתמש. כדאי לעיין בהם כדי לראות את ההנחיות האלה בפועל:
מומלץ בשבילך
- הערה: הטקסט של הקישור מוצג כש-JavaScript מושבת
- ייצור מצבים לממשקי משתמש
- מאחסני מצבים ומצב ממשק המשתמש {:#mad-arch}
- מדריך לארכיטקטורת אפליקציות