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

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

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

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

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

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

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

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

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

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

יכולת שינוי

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

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

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

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

פונקציונליות + UiState.

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

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

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

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

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

מחזיקי מדינה

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

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

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

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

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

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

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

איור 5. ממשק משתמש של פריט בכתבה באפליקציית המקרה לדוגמה.

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

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

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

סוגי לוגיקה

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

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

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

למידע נוסף על בעלי מדינה ועל האופן שבו הם משתלבים בהקשר של שיעזרו לכם ליצור ממשק משתמש, ראו מצב Jetpack Compose guide.

למה כדאי להשתמש ב-UDF?

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

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

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

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

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

צפיות

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

    val uiState: StateFlow<NewsUiState> = 
}

פיתוח נייטיב

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

    val uiState: NewsUiState = 
}

מבוא ל-LiveData כבעלי נתונים גלויים כאן Codelab. במקרה של מבוא לתהליכי Kotlin, ראו תהליכים של קוטלין ב-Android.

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

דרך נפוצה ליצירת סטרימינג של UiState היא חשיפת קובץ גיבוי משתנה כזרם בלתי ניתן לשינוי מ-ViewModel, לדוגמה, חשיפת MutableStateFlow<UiState> בתור StateFlow<UiState>.

צפיות

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

    private val _uiState = MutableStateFlow(NewsUiState())
    val uiState: StateFlow<NewsUiState> = _uiState.asStateFlow()

    ...

}

פיתוח נייטיב

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

    var uiState by mutableStateOf(NewsUiState())
        private set

    ...
}

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

צפיות

class NewsViewModel(
    private val repository: NewsRepository,
    ...
) : ViewModel() {

    private val _uiState = MutableStateFlow(NewsUiState())
    val uiState: StateFlow<NewsUiState> = _uiState.asStateFlow()

    private var fetchJob: Job? = null

    fun fetchArticles(category: String) {
        fetchJob?.cancel()
        fetchJob = viewModelScope.launch {
            try {
                val newsItems = repository.newsItemsForCategory(category)
                _uiState.update {
                    it.copy(newsItems = newsItems)
                }
            } catch (ioe: IOException) {
                // Handle the error and notify the UI when appropriate.
                _uiState.update {
                    val messages = getMessagesFromThrowable(ioe)
                    it.copy(userMessages = messages)
                 }
            }
        }
    }
}

פיתוח נייטיב

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 שבה כל הנכסים זמינים באופן מיידי חשיבותם הולכת וגוברת.

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

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

    • הבדל אחד (UiState): ככל שיש יותר שדות באובייקט UiState, סביר יותר שהזרם יפלוט כתוצאה מאחד מהשדות שלו בתהליך עדכון. כי לצפיות אין מנגנון דיפוזיה כדי להבין אם שיעור הפליטות הרציף הוא שונה או זהה, כל פליטה תגרום לעדכון התצוגה. המשמעות היא שצמצום של השימוש באמצעות Flow ממשקי API או methods כמו distinctUntilChanged() בLiveData, אולי יהיה צורך.

שימוש במצב ממשק המשתמש

כדי לצרוך את השידור של UiState אובייקטים בממשק המשתמש, צריך להשתמש בטרמינל של סוג הנתונים הגלויים שבהם משתמשים. לדוגמה, LiveData משתמשים בשיטה observe(), ובתהליכי Kotlin משתמשים השיטה collect() או הווריאציות שלה.

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

צפיות

class NewsActivity : AppCompatActivity() {

    private val viewModel: NewsViewModel by viewModels()

    override fun onCreate(savedInstanceState: Bundle?) {
        ...

        lifecycleScope.launch {
            repeatOnLifecycle(Lifecycle.State.STARTED) {
                viewModel.uiState.collect {
                    // Update UI elements
                }
            }
        }
    }
}

פיתוח נייטיב

@Composable
fun LatestNewsScreen(
    viewModel: NewsViewModel = viewModel()
) {
    // Show UI elements based on the viewModel.uiState
}

הצגת פעולות שמתבצעות

דרך פשוטה לייצג מצבי טעינה במחלקה UiState היא באמצעות שדה בוליאני:

data class NewsUiState(
    val isFetchingArticles: Boolean = false,
    ...
)

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

צפיות

class NewsActivity : AppCompatActivity() {

    private val viewModel: NewsViewModel by viewModels()

    override fun onCreate(savedInstanceState: Bundle?) {
        ...

        lifecycleScope.launch {
            repeatOnLifecycle(Lifecycle.State.STARTED) {
                // Bind the visibility of the progressBar to the state
                // of isFetchingArticles.
                viewModel.uiState
                    .map { it.isFetchingArticles }
                    .distinctUntilChanged()
                    .collect { progressBar.isVisible = it }
            }
        }
    }
}

פיתוח נייטיב

@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(),
    ...
)

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

שרשור (Threading) ו-בו-זמניות (concurrency)

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

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

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

חלוקה לדפים

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

אנימציות

כדי לספק מעברי ניווט גמישים וחלקים ברמה העליונה, ייתכן רוצים להמתין עד שהמסך השני יטען את הנתונים לפני שמתחילים את האנימציה. מסגרת התצוגה של Android מספקת תכנים מושכים (hooks) לעיכוב מעברים בין מקטעים יעדים עם postponeEnterTransition() וגם startPostponedEnterTransition() ממשקי API. ממשקי ה-API האלה מספקים דרך להבטיח שרכיבי ממשק המשתמש (בדרך כלל תמונה שאוחזרה מהרשת) מוכנים להצגה לפני שממשק המשתמש מוסיף אנימציה של המעבר למסך הזה. לקבלת פרטים נוספים ספציפיים על ההטמעה, ראו את Android Motion דוגמה.

דוגמיות

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