שכבה בממשק המשתמש

תפקיד ממשק המשתמש הוא להציג את נתוני האפליקציה על המסך. ממשק המשתמש משמש גם כנקודת האינטראקציה העיקרית של המשתמשים. בכל פעם שהנתונים משתנים, בגלל אינטראקציה של המשתמש (למשל, לחיצה על כפתור) או קלט חיצוני (למשל, תגובה של הרשת), ממשק המשתמש מתעדכן בהתאם לשינויים האלה. בפועל, ממשק המשתמש הוא ייצוג חזותי של מצב האפליקציה כפי שהוא מאוחזר משכבת הנתונים.

עם זאת, נתוני האפליקציה שמתקבלים משכבת הנתונים הם בדרך כלל בפורמט שונה מהמידע שצריך להציג. לדוגמה, יכול להיות שתצטרכו רק חלק מהנתונים לממשק המשתמש, או שתצטרכו למזג שני מקורות נתונים שונים כדי להציג מידע שרלוונטי למשתמש. לא משנה איזו לוגיקה תיישמו, תצטרכו להעביר לממשק המשתמש את כל המידע שהוא צריך כדי להציג את עצמו באופן מלא. שכבת ממשק המשתמש היא פייפליין שממיר שינויים בנתוני האפליקציה לפורמט שניתן להצגה בממשק המשתמש, ואז מציג אותם.

בארכיטקטורה טיפוסית, רכיבי ממשק המשתמש בשכבת ממשק המשתמש תלויים במחזיקי מצב, שבתורם תלויים במחלקות משכבת הנתונים או משכבת הדומיין האופציונלית.
איור 1. התפקיד של שכבת ממשק המשתמש בארכיטקטורת האפליקציה.

מקרה לדוגמה בסיסי

נניח שיש אפליקציה שמביאה למשתמש כתבות לקריאה. באפליקציה יש מסך מאמרים שבו מוצגים מאמרים שזמינים לקריאה, והיא גם מאפשרת למשתמשים מחוברים להוסיף לסימנייה מאמרים בולטים במיוחד. יכול להיות שיהיו הרבה מאמרים בכל זמן נתון, ולכן הקוראים צריכים להיות מסוגלים לעיין במאמרים לפי קטגוריה. לסיכום, האפליקציה מאפשרת למשתמשים:

  • צפייה במאמרים שזמינים לקריאה.
  • עיון במאמרים לפי קטגוריה.
  • להיכנס לחשבון ולהוסיף כתבות מסוימות לסימניות.
  • גישה לחלק מתכונות הפרימיום אם אתם עומדים בדרישות.
אפליקציית חדשות לדוגמה שבה מוצגות תצוגות מקדימות של מאמרים, שאחד מהם מסומן בסימנייה.
איור 2. אפליקציית חדשות לדוגמה לתרחיש לדוגמה של ממשק משתמש.

בקטעים הבאים נשתמש בדוגמה הזו כבמקרה לדוגמה כדי להציג את העקרונות של זרימת נתונים חד-כיוונית, וגם כדי להמחיש את הבעיות שהעקרונות האלה עוזרים לפתור בהקשר של ארכיטקטורת אפליקציות בשכבת ממשק המשתמש.

ארכיטקטורה של שכבת ממשק המשתמש

המונח ממשק משתמש מתייחס לרכיבי ממשק משתמש כמו קונטיינרים ופונקציות קומפוזביליות שמציגים נתונים. ערכת הכלים המומלצת לבניית ממשקי משתמש ב-Android היא Jetpack פיתוח נייטיב. מאחר שהתפקיד של שכבת הנתונים הוא להחזיק את נתוני האפליקציה, לנהל אותם ולספק גישה אליהם, שכבת ממשק המשתמש צריכה לבצע את השלבים הבאים:

  1. צריכת נתוני האפליקציה והמרתם לנתונים שממשק המשתמש יכול לעבד בקלות.
  2. צריכת נתונים שניתנים לעיבוד בממשק המשתמש והפיכתם לרכיבי ממשק משתמש להצגה למשתמש.
  3. להשתמש באירועי קלט של משתמשים מרכיבי ממשק המשתמש האלה ולשקף את ההשפעות שלהם בנתוני ממשק המשתמש לפי הצורך.
  4. חוזרים על שלבים 1 עד 3 לפי הצורך.

בהמשך המדריך הזה נסביר איך להטמיע שכבת ממשק משתמש שמבצעת את השלבים האלה. במדריך הזה מוסבר על המשימות והמושגים הבאים:

  • איך מגדירים את מצב ממשק המשתמש
  • זרימת נתונים חד-כיוונית (UDF) כאמצעי ליצירה ולניהול של מצב ממשק המשתמש
  • איך חושפים את מצב ממשק המשתמש באמצעות סוגי נתונים שניתנים לצפייה בהתאם לעקרונות של UDF
  • איך מטמיעים ממשק משתמש שמשתמש במצב ממשק המשתמש שניתן לצפייה

ההגדרה הבסיסית ביותר היא של מצב ממשק המשתמש.

הגדרת מצב ממשק המשתמש

במחקר המקרה שצוין קודם, בממשק המשתמש מוצגת רשימה של מאמרים עם מטא-נתונים של כל מאמר. המידע הזה שהאפליקציה מציגה למשתמש הוא מצב ממשק המשתמש.

במילים אחרות, אם ממשק המשתמש הוא מה שהמשתמש רואה, מצב ממשק המשתמש הוא מה שהאפליקציה אומרת שהמשתמש צריך לראות. ממשק המשתמש הוא הייצוג החזותי של מצב ממשק המשתמש, כמו שני צדדים של אותו מטבע. כל שינוי במצב ממשק המשתמש משתקף בו באופן מיידי.

ממשק המשתמש הוא תוצאה של קישור רכיבים בממשק המשתמש במסך למצב ממשק המשתמש.
איור 3. ממשק המשתמש הוא תוצאה של קישור רכיבים בממשק המשתמש במסך למצב ממשק המשתמש.

לדוגמה, כדי לעמוד בדרישות של אפליקציית חדשות, אפשר להגדיר את מחלקת הנתונים 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 פיתוח נייטיב.

אי-שינוי

הגדרת מצב ממשק המשתמש בדוגמה הקודמת היא בלתי ניתנת לשינוי. היתרון העיקרי של זה הוא שאובייקטים שלא ניתן לשנות מספקים ערבויות לגבי מצב האפליקציה בנקודת זמן מסוימת. כך ממשק המשתמש פנוי להתמקד בתפקיד העיקרי שלו: לקרוא את המצב ולעדכן את רכיבי ממשק המשתמש בהתאם. לעולם אל תשנו את מצב ממשק המשתמש ישירות בממשק המשתמש, אלא אם ממשק המשתמש עצמו הוא המקור היחיד של הנתונים שלו. הפרה של העיקרון הזה מובילה לכמה מקורות אמת לאותו פריט מידע, וכתוצאה מכך לחוסר עקביות בנתונים ולבאגים לא בולטים.

לדוגמה, נניח שאתם קוראים את מקרה השימוש שצוין למעלה. אם הדגל bookmarked באובייקט NewsItemUiState ממצב ממשק המשתמש מתעדכן במחלקה Activity, הדגל הזה מתחרה עם שכבת הנתונים כמקור של סטטוס הסימון של מאמר. מחלקות נתונים שלא ניתן לשנות הן שימושיות מאוד למניעת חוסר עקביות מהסוג הזה.

מוסכמות מתן שמות במדריך הזה

במדריך הזה, השמות של מחלקות מצב ממשק המשתמש מבוססים על הפונקציונליות של המסך או של חלק מהמסך שהן מתארות. המוסכמה היא כזו:

functionality + UiState.

לדוגמה, המצב של מסך שבו מוצגות חדשות יכול להיקרא NewsUiState, והמצב של פריט חדשות ברשימה של פריטי חדשות יכול להיות NewsItemUiState.

ניהול מצב באמצעות זרימת נתונים חד-כיוונית

בקטע הקודם הסברנו שמצב ממשק המשתמש הוא תמונת מצב שלא ניתנת לשינוי של הפרטים שדרושים לעיבוד ממשק המשתמש. עם זאת, הנתונים באפליקציות הם דינמיים, ולכן המצב יכול להשתנות עם הזמן. יכול להיות שהסיבה לכך היא אינטראקציה של משתמשים או אירועים אחרים שמשנים את הנתונים הבסיסיים שמשמשים לאכלוס האפליקציה.

כדי לעבד את האינטראקציות האלה, כדאי להשתמש במתווך שיגדיר את הלוגיקה שתחול על כל אירוע ויבצע טרנספורמציה של מקורות הנתונים הבסיסיים כדי ליצור מצב של ממשק המשתמש. אפשר לשכן את האינטראקציות האלה ואת הלוגיקה שלהן בממשק המשתמש עצמו, אבל זה יכול להפוך במהירות למסורבל כי ממשק המשתמש לוקח על עצמו יותר מדי אחריות. בנוסף, זה יכול להשפיע על יכולת הבדיקה כי הקוד שמתקבל הוא קוד מצומד. אלא אם מצב ממשק המשתמש פשוט מאוד, צריך לוודא שהאחריות היחידה של ממשק המשתמש היא לצרוך ולהציג את מצב ממשק המשתמש.

בקטע הזה נדון בזרימת נתונים חד-כיוונית (UDF), תבנית ארכיטקטורה שעוזרת להבטיח את ההפרדה הזו בין תחומי האחריות.

מאחסני מצב

מחזיקי מצב הם המחלקות שאחראיות על יצירת מצב ממשק המשתמש ועל הלוגיקה שנדרשת ליצירת המצב הזה. יש מאגרי מצב בגדלים שונים, בהתאם להיקף של רכיבי ממשק המשתמש התואמים שהם מנהלים. הם יכולים לנהל ווידג'ט יחיד כמו סרגל אפליקציות תחתון, או מסך שלם או יעד ניווט.

במקרה האחרון, ההטמעה הטיפוסית היא מופע של ViewModel, אבל בהתאם לדרישות של האפליקציה, יכול להיות שמחלקה פשוטה תספיק. לדוגמה, באפליקציית החדשות ממקרה לדוגמה, נעשה שימוש במחלקה NewsViewModel כמאחסן מצב כדי ליצור את מצב ממשק המשתמש של המסך שמוצג בקטע הזה.

יש הרבה דרכים ליצור מודל של התלות ההדדית בין ממשק המשתמש לבין הגורם שמייצר את המצב שלו. עם זאת, מכיוון שאפשר להבין את האינטראקציה בין ממשק המשתמש לבין המחלקה שלו בעיקר כקלט של אירוע ופלט של המצב שנובע ממנו, אפשר לייצג את הקשר ביניהם כמו בתרשים הבא:ViewModel

הנתונים של האפליקציה זורמים משכבת הנתונים אל ViewModel. מצב ממשק המשתמש
    זורם מ-ViewModel לרכיבי ממשק המשתמש, ואירועים זורמים מרכיבי
    ממשק המשתמש בחזרה ל-ViewModel.
איור 4. תרשים שמציג איך פונקציה להגדרת משתמש (UDF) פועלת בארכיטקטורת האפליקציה.

הדפוס שבו המצב זורם למטה והאירועים זורמים למעלה נקרא זרימת נתונים חד-כיוונית (UDF). ההשלכות של הדפוס הזה על ארכיטקטורת האפליקציה הן:

  • ה-ViewModel מחזיק את ה-State וחושף אותו לשימוש בממשק המשתמש. מצב ממשק המשתמש הוא נתונים של האפליקציה שעברו טרנספורמציה על ידי ViewModel.
  • ממשק המשתמש מודיע ל-ViewModel על אירועי משתמש.
  • ה-ViewModel מטפל בפעולות המשתמש ומעדכן את המצב.
  • המצב המעודכן מועבר בחזרה לממשק המשתמש לצורך עיבוד.
  • הפעולה הזו חוזרת על עצמה לגבי כל אירוע שגורם לשינוי במצב.

במקרה של יעדי ניווט או מסכים, ה-ViewModel פועל עם מאגרי מידע או עם מחלקות של תרחישי שימוש כדי לקבל נתונים ולהפוך אותם למצב של ממשק המשתמש, תוך שילוב של השפעות האירועים שעשויים לגרום לשינויים במצב. מחקר המקרה שצוין קודם מכיל רשימה של מאמרים, שלכל אחד מהם יש כותרת, תיאור, מקור, שם מחבר, תאריך הוצאה לאור וסימון אם הוא נוסף לסימניות. ממשק המשתמש של כל פריט מאמר נראה כך:

פריט מאמר יחיד מאפליקציית מחקר המקרה. בממשק המשתמש מוצגים תמונה ממוזערת, שם המאמר, המחבר, זמן הקריאה המשוער של המאמר וסמל של סימנייה.
איור 5. ממשק המשתמש של פריט מאמר באפליקציית המחקר.

דוגמה לאירוע שיכול לגרום לשינויים במצב היא משתמש שמבקש להוסיף מאמר לסימניות. בתור יוצר המצב, ה-ViewModel אחראי להגדיר את כל הלוגיקה שנדרשת כדי לאכלס את כל השדות במצב ממשק המשתמש ולעבד את האירועים שנדרשים כדי שממשק המשתמש יוצג באופן מלא.

אירוע בממשק המשתמש מתרחש כשהמשתמש מוסיף מאמר לסימניות. ה-ViewModel
    מודיע לשכבת הנתונים על שינוי הסטטוס. שכבת הנתונים שומרת את השינוי בנתונים ומעדכנת את נתוני האפליקציה. נתוני האפליקציה החדשים עם המאמר שסומן בסימנייה מועברים אל ViewModel, שיוצר את מצב ממשק המשתמש החדש ומעביר אותו לרכיבי ממשק המשתמש לתצוגה.
איור 6. תרשים שממחיש את מחזור האירועים והנתונים ב-UDF.

בקטעים הבאים נבחן מקרוב את האירועים שגורמים לשינויים במצב ואת האופן שבו אפשר לעבד אותם באמצעות UDF.

סוגים של לוגיקה

הוספת מאמר לסימנייה היא דוגמה ללוגיקה עסקית כי היא מוסיפה ערך לאפליקציה. מידע נוסף בנושא זמין בדף שכבת הנתונים. עם זאת, יש סוגים שונים של לוגיקה שחשוב להגדיר:

  • לוגיקה עסקית היא הטמעה של דרישות מוצר לגבי נתוני אפליקציות. כמו שכבר ציינו, דוגמה אחת היא הוספת מאמר לסימנייה באפליקציה של מחקר מקרה. לוגיקה עסקית בדרך כלל ממוקמת בשכבות של הדומיין או הנתונים, אבל אף פעם לא בשכבת ממשק המשתמש.
  • הלוגיקה של התנהגות ממשק המשתמש או הלוגיקה של ממשק המשתמש היא הדרך להציג שינויים במצב במסך. לדוגמה, קבלת הטקסט הנכון להצגה במסך באמצעות Android Resources, ניווט למסך מסוים כשהמשתמש לוחץ על לחצן או הצגת הודעה למשתמש במסך באמצעות toast או snackbar.

חשוב לשמור את הלוגיקה של ממשק המשתמש בממשק המשתמש ולא ב-ViewModel, במיוחד כשמדובר בסוגי ממשק משתמש כמו Context. אם ממשק המשתמש הופך למורכב מדי ואתם רוצים להעביר את הלוגיקה של ממשק המשתמש למחלקה אחרת כדי להקל על הבדיקות ולשמור על הפרדה בין התחומים, אתם יכולים ליצור מחלקה פשוטה בתור מחזיק מצב. מחלקות פשוטות שנוצרו בממשק המשתמש יכולות להסתמך על Android SDK כי הן פועלות לפי מחזור החיים של ממשק המשתמש. לאובייקטים מסוג ViewModel יש משך חיים ארוך יותר.

מידע נוסף על מחזיקי מצב ועל האופן שבו הם משתלבים בהקשר של עזרה בבניית ממשק משתמש זמין במדריך Jetpack פיתוח נייטיב Compose State.

למה כדאי להשתמש בפונקציות UDF?

מודל ה-UDF מתאר את מחזור הייצור של מצב, כפי שמוצג באיור 4. הוא גם מפריד בין המקום שבו מתחילים שינויים במצב, המקום שבו הם עוברים טרנספורמציה והמקום שבו הם נצרכים בסופו של דבר. ההפרדה הזו מאפשרת לממשק המשתמש לעשות בדיוק את מה שהשם שלו מרמז: להציג מידע על ידי מעקב אחרי שינויים בסטטוס, ולהעביר את כוונת המשתמש על ידי העברת השינויים האלה אל ViewModel.

במילים אחרות, UDF מאפשר את הפעולות הבאות:

  • עקביות הנתונים. יש מקור מהימן אחד לממשק המשתמש.
  • יכולת בדיקה. מקור המצב מבודד ולכן אפשר לבדוק אותו בלי קשר לממשק המשתמש.
  • יכולת תחזוקה. שינוי המצב מתבצע לפי דפוס מוגדר היטב, שבו השינויים הם תוצאה של אירועים שקשורים למשתמשים ושל מקורות הנתונים שהם שולפים מהם.

חשיפת מצב ממשק המשתמש

אחרי שמגדירים את מצב ממשק המשתמש ומחליטים איך לנהל את הייצור של המצב הזה, השלב הבא הוא להציג את המצב שנוצר בממשק המשתמש.

כשמשתמשים ב-UDF כדי לנהל את הייצור של מצב, אפשר להתייחס למצב שנוצר כאל זרם – במילים אחרות, נוצרות כמה גרסאות של המצב לאורך זמן. חשיפת מצב ממשק המשתמש במאגר נתונים שניתן לצפייה כמו StateFlow. כך ממשק המשתמש יכול להגיב לכל שינוי במצב בלי שתצטרכו לשלוף נתונים ישירות מ-ViewModel באופן ידני. בנוסף, היתרון של הגישה הזו הוא שתמיד יש במטמון את הגרסה העדכנית של מצב ממשק המשתמש, וזה שימושי לשחזור מהיר של המצב אחרי שינויים בהגדרות.

class NewsViewModel(...) : ViewModel() {

    val uiState: NewsUiState = 
}

למבוא ל-Kotlin Flows, אפשר לעיין במאמר Kotlin Flows ב-Android. כדי ללמוד איך להשתמש ב-StateFlow כמאגר נתונים גלויים, אפשר לעיין בשיעור Codelab בנושא מצב מתקדם ותופעות לוואי ב-Jetpack פיתוח נייטיב.

במקרים שבהם הנתונים שמוצגים בממשק המשתמש הם יחסית פשוטים, כדאי לעטוף את הנתונים בסוג של מצב ממשק משתמש, כי הוא מעביר את הקשר בין הפליטה של מחזיק המצב לבין המסך או רכיב ממשק המשתמש שמשויכים אליו. ככל שרכיב ממשק המשתמש נעשה מורכב יותר, קל להוסיף אותו להגדרה של מצב ממשק המשתמש, כך שאפשר להתאים את המידע הנוסף שנדרש לעיבוד רכיב ממשק המשתמש.

דרך נפוצה ליצור stream של 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, כך גדל הסיכוי שהסטרים ישודר כתוצאה מעדכון של אחד מהשדות שלו. לרכיבי ממשק המשתמש אין מנגנון השוואה כדי להבין אם פליטות עוקבות הן שונות או זהות, ולכן כל פליטה גורמת לעדכון של רכיב ממשק המשתמש. המשמעות היא שאולי יהיה צורך בשימוש ב-methods של Flow API כמו 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 זמין במאמר שגרות משנה ב-Kotlin ב-Android.

שינויים בניווט באפליקציה נובעים לרוב מפליטות דמויות אירועים. לדוגמה, אחרי שמשתמש בכיתה SignInViewModel מבצע כניסה, יכול להיות שבמשתמש UiState השדה isSignedIn מוגדר לערך true. השימוש בטריגרים כאלה דומה לשימוש בטריגרים שמתואר בקטע הקודם בנושא Consume UI state, אבל ההטמעה של השימוש מתבצעת בNavigation component.

מידע נוסף על ניווט בממשק המשתמש זמין במאמר ניווט 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 ו-Shared element transitions in Compose.

מקורות מידע נוספים

צפייה בתוכן

דוגמאות

בדוגמאות הבאות של Google מוצג השימוש בשכבת ממשק המשתמש. כדאי לעיין בהם כדי לראות איך ההנחיות האלה באות לידי ביטוי בפועל: