התפקיד של ממשק המשתמש הוא להציג את נתוני האפליקציה במסך. ממשק המשתמש הוא גם המקום העיקרי שבו המשתמשים יכולים לבצע פעולות. בכל פעם שהנתונים משתנים, בגלל אינטראקציה של המשתמש (כמו לחיצה על כפתור) או קלט חיצוני (כמו תגובה מהרשת), ממשק המשתמש מתעדכן בהתאם לשינויים האלה. למעשה, ממשק המשתמש הוא ייצוג חזותי של מצב האפליקציה כפי שהוא מאוחזר משכבת הנתונים.
עם זאת, נתוני האפליקציה שאתם מקבלים משכבת הנתונים הם בדרך כלל בפורמט שונה מהמידע שאתם צריכים להציג. לדוגמה, יכול להיות שתצטרכו רק חלק מהנתונים לממשק המשתמש, או שתצטרכו למזג שני מקורות נתונים שונים כדי להציג מידע שרלוונטי למשתמש. לא משנה באיזה לוגיקה אתם משתמשים, אתם צריכים להעביר לממשק המשתמש את כל המידע שנדרש כדי להציג אותו באופן מלא. שכבת ממשק המשתמש היא הצינור שמעביר את השינויים בנתוני האפליקציה לפורמט שאפשר להציג בממשק המשתמש, ואז מציג אותם.
מקרה לדוגמה בסיסי
נניח שיש אפליקציה שמאחזרת כתבות חדשותיות שהמשתמש יכול לקרוא. באפליקציה יש מסך מאמרים שבו מוצגים מאמרים שזמינים לקריאה, ומשתמשים שמחוברים לחשבון יכולים גם להוסיף לסימניות מאמרים שבאמת בולטים. יכול להיות שיהיו הרבה מאמרים בכל זמן נתון, לכן הקוראים צריכים להיות מסוגלים לעיין במאמרים לפי קטגוריות. לסיכום, האפליקציה מאפשרת למשתמשים:
- לראות את המאמרים שזמינים לקריאה.
- לעיין במאמרים לפי קטגוריה.
- להיכנס לחשבון ולהוסיף כתבות מסוימות לסימניות.
- גישה לחלק מתכונות הפרימיום, אם אתם עומדים בדרישות.
בקטעים הבאים נשתמש בדוגמה הזו כמקרה בוחן כדי להציג את העקרונות של זרימת נתונים חד-כיוונית, וגם כדי להמחיש את הבעיות שהעקרונות האלה עוזרים לפתור בהקשר של ארכיטקטורת האפליקציה לשכבת ממשק המשתמש.
ארכיטקטורת שכבת ממשק המשתמש
המונח ממשק משתמש מתייחס לרכיבים בממשק המשתמש, כמו קונטיינרים ופונקציות קומפוזביליות שמציגות נתונים. כדי ליצור ממשקי משתמש ב-Android, ערכת הכלים המומלצת היא Jetpack Compose. התפקיד של שכבת הנתונים הוא להחזיק את נתוני האפליקציה, לנהל אותם ולספק גישה אליהם, ולכן שכבת ממשק המשתמש צריכה לבצע את הפעולות הבאות:
- לצרוך נתוני אפליקציה ולהפוך אותם לנתונים שממשק המשתמש יכול לעבד בקלות.
- לצרוך נתונים שניתן לעבד בממשק המשתמש ולהפוך אותם לרכיבים בממשק המשתמש כדי להציג אותם למשתמש.
- לצרוך אירועי קלט של משתמשים מרכיבי ממשק המשתמש שהורכבו, ולשקף את ההשפעות שלהם בנתוני ממשק המשתמש לפי הצורך.
- חוזרים על שלבים 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)
בקטע הקודם הסברנו שמצב ממשק המשתמש הוא תמונת מצב שלא ניתנת לשינוי של הפרטים שנדרשים כדי לעבד את ממשק המשתמש. עם זאת, הנתונים באפליקציות הם דינמיים, ולכן המצב יכול להשתנות עם הזמן. יכול להיות שהסיבה לכך היא אינטראקציה של המשתמש או אירועים אחרים שמשנים את הנתונים הבסיסיים שמשמשים לאכלוס האפליקציה.
האינטראקציות האלה יכולות להפיק תועלת ממתווך שיעבד אותן, יגדיר את הלוגיקה שתחול על כל אירוע וישנה את מקורות הנתונים התומכים כדי ליצור מצב של ממשק משתמש. אפשר לאחסן את האינטראקציות האלה ואת הלוגיקה שלהן בממשק המשתמש עצמו, אבל זה יכול להפוך במהירות למסורבל כי ממשק המשתמש לוקח על עצמו יותר מדי אחריות. בנוסף, זה יכול להשפיע על יכולת הבדיקה כי הקוד שנוצר הוא צימוד חזק. אלא אם מצב ממשק המשתמש פשוט מאוד, ודאו שהאחריות היחידה של ממשק המשתמש היא לצרוך ולהציג את מצב ממשק המשתמש.
בקטע הזה נדון בזרימת נתונים חד-כיוונית (UDF), דפוס ארכיטקטורה שעוזר לאכוף את ההפרדה הבריאה הזו של תחומי האחריות.
מאחסני מצב
מאחסני מצב הם המחלקות שאחראיות על יצירת מצב ממשק המשתמש ועל הלוגיקה שנדרשת כדי ליצור את המצב הזה. מאחסני מצב מגיעים בגדלים שונים בהתאם להיקף של הרכיבים המתאימים בממשק המשתמש שהם מנהלים, החל מווידג'ט יחיד כמו סרגל אפליקציה תחתון ועד למסך שלם או יעד ניווט.
במקרה השני, ההטמעה הרגילה היא מופע של ViewModel, אבל בהתאם לדרישות של האפליקציה, יכול להיות שמחלקה פשוטה תספיק. לדוגמה, באפליקציית החדשות ממקרה המחקר נעשה שימוש במחלקה NewsViewModel כמאחסן מצב כדי ליצור את מצב ממשק המשתמש של המסך שמוצג בקטע הזה.
יש הרבה דרכים ליצור מודל של התלות ההדדית בין ממשק המשתמש לבין המפיק של המצב שלו. עם זאת, מכיוון שהאינטראקציה בין ממשק המשתמש לבין המחלקה ViewModel שלו יכולה להיות מובנת במידה רבה כקלט של אירוע ופלט המצב שנובע ממנו, אפשר לייצג את הקשר כפי שמוצג בתרשים הבא:
התבנית שבה המצב זורם למטה והאירועים זורמים למעלה נקראת זרימת נתונים חד-כיוונית (UDF). ההשלכות של התבנית הזו על ארכיטקטורת האפליקציה הן:
- ה-ViewModel מכיל את המצב וחושף אותו לצריכה על ידי ממשק המשתמש. מצב ממשק המשתמש הוא נתוני האפליקציה שעברו טרנספורמציה על ידי ה-ViewModel.
- ממשק המשתמש מודיע ל-ViewModel על אירועי משתמש.
- ה-ViewModel מטפל בפעולות של המשתמש ומעדכן את המצב.
- המצב המעודכן מועבר בחזרה לממשק המשתמש כדי לעבד אותו.
- התהליך שלמעלה חוזר על עצמו בכל אירוע שגורם לשינוי במצב.
במסכים או ביעדי ניווט, ה-ViewModel פועל עם מאגרים או עם מחלקות של תרחישי שימוש כדי לקבל נתונים ולהפוך אותם למצב של ממשק המשתמש, תוך שילוב ההשפעות של אירועים שעשויים לגרום לשינויים במצב. במקרה לדוגמה שמוזכר למעלה יש רשימה של מאמרים, ולכל אחד מהם יש כותרת, תיאור, מקור, שם מחבר, תאריך הוצאה לאור וסימון אם הוא נוסף לסימניות. ממשק המשתמש של כל פריט מאמר נראה כך:
דוגמה לאירוע שיכול לגרום לשינויים במצב היא משתמש שמבקש להוסיף מאמר לסימניות. כמפיק המצב, ה-ViewModel אחראי להגדיר את כל הלוגיקה שנדרשת כדי לאכלס את כל השדות במצב של ממשק המשתמש ולעבד את האירועים שנדרשים כדי לרנדר את ממשק המשתמש באופן מלא.
בקטעים הבאים נסביר על האירועים שגורמים לשינויים בסטטוס ואיך אפשר לעבד אותם באמצעות פונקציה מוגדרת על ידי המשתמש (UDF).
סוגי לוגיקה
הוספת סימנייה למאמר היא דוגמה ללוגיקה עסקית כי היא מוסיפה ערך לאפליקציה. מידע נוסף זמין בדף שכבת נתונים. עם זאת, יש סוגים שונים של לוגיקה שחשוב להגדיר:
- לוגיקה עסקית היא ההטמעה של דרישות המוצר לנתוני האפליקציה. כפי שכבר צוין, דוגמה אחת היא הוספת סימנייה למאמר באפליקציית ה-case study. לוגיקה עסקית ממוקמת בדרך כלל בשכבות הדומיין או הנתונים, אבל אף פעם לא בשכבת ממשק המשתמש.
- לוגיקת ההתנהגות של ממשק המשתמש או לוגיקת ממשק המשתמש היא האופן שבו שינויים במצב מוצגים במסך. לדוגמה, קבלת הטקסט הנכון להצגה במסך באמצעות
Resourcesשל Android, ניווט למסך מסוים כשהמשתמש לוחץ על כפתור או הצגת הודעה למשתמש במסך באמצעות הודעת Toast או סרגל חטיפים.
חשוב לשמור את הלוגיקה של ממשק המשתמש בממשק המשתמש, ולא ב-ViewModel, במיוחד כשהיא כוללת סוגים של ממשק משתמש כמו Context.
אם ממשק המשתמש הופך למורכב יותר ואתם רוצים להעביר את הלוגיקה של ממשק המשתמש למחלקה אחרת כדי לשפר את יכולת הבדיקה ואת ההפרדה בין תחומי האחריות, אתם יכולים ליצור מחלקה פשוטה בתור מאחסן מצב. מחלקות פשוטות שנוצרות בממשק המשתמש יכולות לקבל יחסי תלות של Android SDK כי הן פועלות לפי מחזור החיים של ממשק המשתמש. לאובייקטים של ViewModel יש תוחלת חיים ארוכה יותר.
מידע נוסף על מאחסני מצב ואיך הם משתלבים בהקשר של עזרה בפיתוח ממשק משתמש זמין במדריך בנושא מצב ב-Jetpack Compose.
למה כדאי להשתמש ב-UDF?
מודל UDF ממדל את מחזור הייצור של המצב, כמו שמוצג באיור 4. הוא גם מפריד בין המקום שבו מתחילים השינויים במצב, המקום שבו הם משתנים והמקום שבו הם נצרכים בסופו של דבר. ההפרדה הזו מאפשרת לממשק המשתמש לעשות בדיוק את מה שהשם שלו מרמז: להציג מידע על ידי מעקב אחרי שינויים במצב, ולהעביר את כוונת המשתמש על ידי העברת השינויים האלה ל-ViewModel.
במילים אחרות, UDF מאפשר:
- עקביות הנתונים. יש מקור מהימן אחד לממשק המשתמש.
- יכולת בדיקה. מקור המצב מבודד, ולכן אפשר לבדוק אותו בלי קשר לממשק המשתמש.
- יכולת תחזוקה. שינוי המצב מתבצע לפי דפוס מוגדר היטב, שבו השינויים הם תוצאה של אירועי משתמשים ושל מקורות הנתונים שהם שולפים מהם.
חשיפת מצב ממשק המשתמש
אחרי שמגדירים את מצב ממשק המשתמש וקובעים איך לנהל את הייצור של המצב הזה, השלב הבא הוא להציג את המצב שנוצר בממשק המשתמש.
כשמשתמשים ב-UDF כדי לנהל את הפקת המצב, אפשר להתייחס למצב שנוצר כאל זרם – כלומר, נוצרות כמה גרסאות של המצב לאורך זמן. חושפים את מצב ממשק המשתמש במאגר נתונים שניתן לצפות בו, כמו StateFlow. כך ממשק המשתמש יכול להגיב לכל שינוי שנעשה במצב בלי שתצטרכו לשלוף נתונים באופן ידני ישירות מ-ViewModel. בנוסף, כך הגרסה העדכנית של מצב ממשק המשתמש תמיד נשמרת במטמון, וזה שימושי לשחזור מהיר של המצב אחרי שינויים בהגדרות.
class NewsViewModel( // ... ) : ViewModel() { val uiState: NewsUiState = /* ... */ }
למבוא לתהליכים ב-Kotlin, ראו תהליכים ב-Kotlin ב-Android.
כדי ללמוד איך להשתמש ב-StateFlow כמחזיק נתונים שניתן לצפות בהם, עיינו ב-Codelab בנושא מצב מתקדם ותופעות לוואי ב-Jetpack פיתוח נייטיב.
במקרים שבהם הנתונים שמוצגים בממשק המשתמש הם יחסית פשוטים, בדרך כלל כדאי לעטוף את הנתונים בסוג של מצב ממשק משתמש, כי הוא מעביר את הקשר בין הפליטה של מאחסן המצב לבין המסך או הרכיב בממשק המשתמש שמשויכים אליו. ככל שרכיב ממשק המשתמש הופך למורכב יותר, קל להוסיף להגדרה של מצב ממשק המשתמש, כדי שתוכלו להתאים את המידע הנוסף שנדרש לעיבוד של רכיב ממשק המשתמש.
דרך נפוצה ליצור זרם של UiState היא לחשוף מאפיין mutableStateOf עם private set, כך שהמצב יהיה ניתן לשינוי בתוך ה-ViewModel אבל לקריאה בלבד בממשק המשתמש.
class NewsViewModel( // ... ) : ViewModel() { var uiState by mutableStateOf(NewsUiState()) private set // ... }
לאחר מכן, ה-ViewModel יכול לחשוף שיטות שמשנות את המצב באופן פנימי, ולפרסם עדכונים שהממשק יכול לצרוך. לדוגמה, נניח שאתם צריכים לבצע פעולה אסינכרונית.
אפשר להפעיל פונקציית coroutine באמצעות 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, כך גדל הסיכוי שהסטרימינג יופעל כתוצאה מעדכון של אחד מהשדות שלו. לרכיבים בממשק המשתמש אין מנגנון השוואה כדי להבין אם פליטות עוקבות שונות או זהות, ולכן כל פליטה גורמת לעדכון של הרכיב בממשק המשתמש. זה אומר שיכול להיות שתצטרכו להשתמש בשיטות שלFlowAPI כמוdistinctUntilChanged()כדי להפחית את הסיכון.
מידע נוסף על עיבוד ועל מצב ממשק המשתמש זמין במאמר מחזור החיים של רכיבים קומפוזביליים.
שימוש במצב ממשק המשתמש
כדי לצרוך את הזרם של אובייקטים מסוג UiState בממשק המשתמש, צריך להשתמש באופרטור הסופי של סוג הנתונים הגלויים שבו אתם משתמשים. לדוגמה, בתהליכים של Kotlin, משתמשים ב-method collect() או בווריאציות שלו.
כשצורכים מחזיקי נתונים גלויים בממשק המשתמש, חשוב להביא בחשבון את מחזור החיים של ממשק המשתמש. אל תגרמו לממשק המשתמש לצפות בערך הדינמי של ממשק המשתמש כשהפונקציה הקומפוזבילית לא מוצגת למשתמש. מידע נוסף על הנושא הזה זמין בפוסט הזה בבלוג. כשמשתמשים בתהליכים, מומלץ לטפל בבעיות שקשורות למחזור החיים באמצעות היקף הקורוטינה המתאים ו-API של collectAsStateWithLifecycle:
@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 מספקים תמיכה מובנית בהן. מידע נוסף על השימוש בשגרות המשך באפליקציות ל-Android זמין במאמר שגרות המשך ב-Kotlin ב-Android.
ניווט
שינויים בניווט באפליקציה נובעים לעיתים קרובות מפליטות דמויות אירועים. לדוגמה, אחרי שמבצעים כניסה באמצעות מחלקה מסוג SignInViewModel, יכול להיות שיוגדר בשדה isSignedIn של UiState הערך true. צורכים טריגרים כאלה בדיוק כמו אלה שנדונו בקטע הקודם צריכת מצב ממשק המשתמש, אבל דוחים את הטמעת הצריכה לרכיב הניווט.
מידע נוסף על הניווט בממשק המשתמש זמין במאמר ניווט 3.
חלוקה לדפים
ספריית Paging נצרכת בממשק המשתמש עם סוג שנקרא 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 ומעברים של רכיבים משותפים ב-Compose.
מקורות מידע נוספים
צפייה בתוכן
דוגמאות
הדוגמאות הבאות של Google ממחישות את השימוש בשכבת ממשק המשתמש. כדאי לעיין בהם כדי לראות את ההנחיות האלה בפועל:
מומלץ בשבילך
- הערה: טקסט הקישור מוצג כש-JavaScript מושבת
- ייצור מצבים לממשקי משתמש
- מאחסני מצבים ומצב ממשק המשתמש {:#mad-arch}
- מדריך לארכיטקטורת אפליקציות