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

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

הנתונים זורמים בכיוון אחד משכבת הנתונים לממשק המשתמש.
איור 1. זרימת נתונים חד-כיוונית.

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

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

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

רכיבים של צינור העיבוד לייצור מצבים לממשקי משתמש

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

מצב ממשק המשתמש

מצב ממשק המשתמש הוא המאפיין שמתאר את ממשק המשתמש. יש שני סוגים של מצב ממשק המשתמש:

  • מצב ממשק המשתמש במסך הוא מה שצריך להציג במסך. לדוגמה, מחלקת NewsUiState יכולה להכיל את מאמרי החדשות ומידע אחר שנדרש כדי להציג את ממשק המשתמש. המצב הזה בדרך כלל קשור לשכבות אחרות בהיררכיה כי הוא מכיל נתוני האפליקציה.
  • מצב של רכיב בממשק המשתמש מתייחס למאפיינים שמוגדרים ברכיבים בממשק המשתמש ומשפיעים על אופן העיבוד שלהם. יכול להיות שרכיב בממשק המשתמש יוצג או יוסתר, ויהיה לו גופן, גודל גופן או צבע גופן מסוימים. ב-Jetpack Compose, המצב הוא חיצוני לרכיב הקומפוזבילי, ואפשר אפילו להעביר אותו אל מחוץ לסביבה הקרובה של הרכיב הקומפוזבילי אל פונקציית הרכיב הקומפוזבילי שקוראת לו או אל מחזיק המצב. דוגמה לכך היא ScaffoldState עבור הרכיב הקומפוזבילי Scaffold.

לוגיקה

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

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

הלוגיקה באפליקציה יכולה להיות לוגיקה עסקית או לוגיקת ממשק משתמש:

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

מחזור החיים של Android והסוגים של מצב הממשק והלוגיקה

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

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

הטבלה הבאה מסכמת את האפשרויות שצוינו למעלה:

מחזור החיים של ממשק המשתמש לא תלוי תלוי במחזור החיים של ממשק המשתמש
לוגיקה עסקית הלוגיקה של ממשק המשתמש
מצב ממשק המשתמש במסך

צינור העיבוד של מצבי ממשק המשתמש

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

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

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

    @Composable
    fun Counter() {
        // The UI state is managed by the UI itself
        var count by remember { mutableStateOf(0) }
        Row {
            Button(onClick = { ++count }) {
                Text(text = "Increment")
            }
            Button(onClick = { --count }) {
                Text(text = "Decrement")
            }
        }
    }
    
  • לוגיקה של ממשק המשתמש ← ממשק משתמש. לדוגמה, הצגה או הסתרה של לחצן שמאפשר למשתמש לעבור לראש הרשימה.

    @Composable
    fun ContactsList(contacts: List<Contact>) {
        val listState = rememberLazyListState()
        val isAtTopOfList by remember {
            derivedStateOf {
                listState.firstVisibleItemIndex < 3
            }
        }
    
        // Create the LazyColumn with the lazyListState
        ...
    
        // Show or hide the button (UI logic) based on the list scroll position
        AnimatedVisibility(visible = !isAtTopOfList) {
            ScrollToTopButton()
        }
    }
    
  • לוגיקה עסקית ← ממשק משתמש. רכיב בממשק המשתמש שבו מוצגת התמונה של המשתמש הנוכחי במסך.

    @Composable
    fun UserProfileScreen(viewModel: UserProfileViewModel = hiltViewModel()) {
        // Read screen UI state from the business logic state holder
        val uiState by viewModel.uiState.collectAsStateWithLifecycle()
    
        // Call on the UserAvatar Composable to display the photo
        UserAvatar(picture = uiState.profilePicture)
    }
    
  • לוגיקה עסקית ← לוגיקה של ממשק המשתמש ← ממשק המשתמש. רכיב בממשק המשתמש שניתן לגלול בו כדי להציג את המידע הנכון במסך עבור מצב מסוים של ממשק המשתמש.

    @Composable
    fun ContactsList(viewModel: ContactsViewModel = hiltViewModel()) {
        // Read screen UI state from the business logic state holder
        val uiState by viewModel.uiState.collectAsStateWithLifecycle()
        val contacts = uiState.contacts
        val deepLinkedContact = uiState.deepLinkedContact
    
        val listState = rememberLazyListState()
    
        // Create the LazyColumn with the lazyListState
        ...
    
        // Perform UI logic that depends on information from business logic
        if (deepLinkedContact != null && contacts.isNotEmpty()) {
            LaunchedEffect(listState, deepLinkedContact, contacts) {
                val deepLinkedContactIndex = contacts.indexOf(deepLinkedContact)
                if (deepLinkedContactIndex >= 0) {
                  // Scroll to deep linked item
                  listState.animateScrollToItem(deepLinkedContactIndex)
                }
            }
        }
    }
    

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

הנתונים זורמים משכבת הפקת הנתונים אל ממשק המשתמש
איור 3. החלת לוגיקה בשכבת ממשק המשתמש.

בעלי סטטוס ותחומי האחריות שלהם

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

היתרונות של השיטה הזו:

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

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

סוגים של בעלי תפקידים במדינה

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

  • מאחסן מצב הלוגיקה העסקית.
  • מאחסן המצב של לוגיקת ממשק המשתמש.

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

לוגיקה עסקית ומאחסן המצב שלה

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

מאפיין (property) פרטים
ייצור מצב ממשק משתמש האחריות על יצירת מצב ממשק המשתמש עבור ממשקי המשתמש שלהם מוטלת על מחזיקי המצב של הלוגיקה העסקית. מצב ממשק המשתמש הזה הוא לרוב תוצאה של עיבוד אירועי משתמשים וקריאת נתונים משכבות הדומיין והנתונים.
הנתונים נשמרים באמצעות יצירה מחדש של פעילות ה-state holders של הלוגיקה העסקית שומרים על המצב שלהם ועל צינורות העיבוד של המצב במהלך Activity יצירה מחדש, וכך עוזרים לספק חוויית משתמש חלקה. במקרים שבהם אי אפשר לשמור את הנתונים של מאחסן המצב והוא נוצר מחדש (בדרך כלל אחרי השבתת תהליך), מאחסן המצב צריך להיות מסוגל ליצור מחדש בקלות את המצב האחרון שלו כדי להבטיח חוויית משתמש עקבית.
Possess long lived state לרוב משתמשים ב-state holders של לוגיקה עסקית כדי לנהל את הסטטוס של יעדי ניווט. לכן, ברוב המקרים הם שומרים את המצב שלהם גם אחרי שינויים בניווט, עד שהם מוסרים מתרשים הניווט.
ייחודית לממשק המשתמש שלה ואי אפשר לעשות בה שימוש חוזר בדרך כלל, מחזיקי מצב של לוגיקה עסקית יוצרים מצב עבור פונקציה מסוימת באפליקציה, לדוגמה TaskEditViewModel או TaskListViewModel, ולכן הם רלוונטיים רק לפונקציה הזו באפליקציה. אותו מאחסן מצב יכול לתמוך בתכונות האפליקציה האלה במגוון גורמי צורה. לדוגמה, גרסאות של האפליקציה לנייד, לטלוויזיה ולטאבלט יכולות לעשות שימוש חוזר באותו מאחסן מצב של לוגיקה עסקית.

לדוגמה, נסתכל על יעד הניווט של המחבר באפליקציה Now in Android:

באפליקציה Now in Android מוצג איך יעד ניווט שמייצג פונקציה מרכזית באפליקציה צריך לכלול מחזיק מצב ייחודי משלו של לוגיקה עסקית.
איור 4. אפליקציית Now in Android.

ה-AuthorViewModel משמש כמאחסן מצב של הלוגיקה העסקית, ובמקרה הזה הוא יוצר את מצב ממשק המשתמש:

@HiltViewModel
class AuthorViewModel @Inject constructor(
    savedStateHandle: SavedStateHandle,
    private val authorsRepository: AuthorsRepository,
    newsRepository: NewsRepository
) : ViewModel() {

    val uiState: StateFlow<AuthorScreenUiState> = 

    // Business logic
    fun followAuthor(followed: Boolean) {
      
    }
}

שימו לב של-AuthorViewModel יש את המאפיינים שצוינו קודם:

מאפיין (property) פרטים
התוצאה היא AuthorScreenUiState AuthorViewModel קורא נתונים מ-AuthorsRepository ומ-NewsRepository ומשתמש בנתונים האלה כדי ליצור AuthorScreenUiState. הוא גם מחיל לוגיקה עסקית כשהמשתמש רוצה לעקוב אחרי Author או להפסיק לעקוב אחריו, על ידי העברה אל AuthorsRepository.
יש גישה לשכבת הנתונים מועבר אליו מופע של AuthorsRepository ושל NewsRepository בקונסטרוקטור שלו, וכך הוא יכול להטמיע את הלוגיקה העסקית של מעקב אחרי Author.
הסרטון שורד Activity יצירה מחדש מכיוון שהיא מיושמת באמצעות ViewModel, היא תישמר גם אחרי יצירה מהירה של Activity. במקרה של סיום התהליך, אפשר לקרוא את האובייקט SavedStateHandle כדי לקבל את כמות המידע המינימלית שנדרשת לשחזור מצב ממשק המשתמש משכבת הנתונים.
Possesses long lived state ה-ViewModel מוגדר לגרף הניווט, ולכן אלא אם יעד המחבר יוסר מגרף הניווט, מצב ממשק המשתמש ב-uiState StateFlow יישאר בזיכרון. השימוש ב-StateFlow מוסיף גם את היתרון של יישום עצל של הלוגיקה העסקית שמייצרת את המצב, כי המצב נוצר רק אם יש אוסף של מצב ממשק המשתמש.
ייחודי לממשק המשתמש שלו ההנחה AuthorViewModel רלוונטית רק ליעד הניווט של המחבר, ואי אפשר להשתמש בה שוב בשום מקום אחר. אם יש לוגיקה עסקית שנעשה בה שימוש חוזר ביעדי ניווט שונים, צריך להגדיר את הלוגיקה העסקית הזו כרכיב עם היקף של שכבת נתונים או שכבת דומיין.

‫ViewModel כמאחסן מצב של הלוגיקה העסקית

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

  • פעולות שמופעלות על ידי ViewModels שורדות שינויים בהגדרות.
  • שילוב עם ניווט:
    • הניווט שומר במטמון את ה-ViewModels בזמן שהמסך נמצא במקבץ פעילויות קודמות (back stack). הפעולה הזו חשובה כדי שהנתונים שנטענו קודם יהיו זמינים באופן מיידי כשחוזרים ליעד. קשה יותר לעשות את זה עם מחזיק מצב שפועל לפי מחזור החיים של המסך הקומפוזבילי.
    • ה-ViewModel גם מתנקה כשמוציאים את היעד מה-back stack, וכך המצב מתנקה באופן אוטומטי. זה שונה מהאזנה לסילוק של רכיב שאפשר להשתמש בו שוב, שיכול לקרות מכמה סיבות, כמו מעבר למסך חדש, שינוי בהגדרה או סיבות אחרות.
  • שילוב עם ספריות אחרות של Jetpack, כמו Hilt.

לוגיקת ממשק המשתמש ומאחסן המצב שלה

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

  • יוצר מצב של ממשק המשתמש ומנהל את המצב של רכיבי ממשק המשתמש.
  • לא שורד Activity יצירה מחדש: מחזיקי מצב שמארחים בלוגיקה של ממשק המשתמש תלויים לעיתים קרובות במקורות נתונים מממשק המשתמש עצמו, וניסיון לשמור את המידע הזה בשינויים בתצורה גורם בדרך כלל לדליפת זיכרון. אם בעלי מצב צריכים שהנתונים יישמרו גם אחרי שינויים בהגדרות, הם צריכים להעביר את הטיפול לרכיב אחר שמתאים יותר לשמירה אחרי יצירה מחדש.Activity לדוגמה, ב-Jetpack פיתוח נייטיב, מצבים של רכיבי ממשק משתמש קומפוזביליים שנוצרו באמצעות פונקציות remembered מועברים לעיתים קרובות לפונקציה rememberSaveable כדי לשמור את המצב במהלך יצירה מחדש של Activity. דוגמאות לפונקציות כאלה כוללות rememberScaffoldState() ו-rememberLazyListState().
  • יש הפניות למקורות נתונים בהיקף ממשק המשתמש: אפשר להפנות למקורות נתונים כמו ממשקי API של מחזור החיים ומשאבים ולקרוא אותם בצורה בטוחה, כי למאחסן מצב הלוגיקה של ממשק המשתמש יש את אותו מחזור חיים כמו לממשק המשתמש.
  • אפשר להשתמש בו שוב בכמה ממשקי משתמש: אפשר להשתמש שוב במופעים שונים של אותו מחזיק מצב לוגי של ממשק משתמש בחלקים שונים של האפליקציה. לדוגמה, אפשר להשתמש במחזיק מצב לניהול אירועי קלט של משתמשים בקבוצת צ'יפים בדף חיפוש לצ'יפים של מסננים, וגם בשדה 'אל' של נמעני אימייל.

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

הדוגמה הבאה מתוך הדוגמה Now in Android ממחישה את מה שצוין למעלה:

המאמר Now in Android משתמש במאחסן מצבים של מחלקה רגילה כדי לנהל את הלוגיקה של ממשק המשתמש
איור 5. אפליקציית הדוגמה Now in Android.

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

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

@Stable
class NiaAppState(
    val navController: NavHostController,
    val windowSizeClass: WindowSizeClass
) {

    // UI logic
    val shouldShowBottomBar: Boolean
        get() = windowSizeClass.widthSizeClass == WindowWidthSizeClass.Compact ||
            windowSizeClass.heightSizeClass == WindowHeightSizeClass.Compact

    // UI logic
    val shouldShowNavRail: Boolean
        get() = !shouldShowBottomBar

   // UI State
    val currentDestination: NavDestination?
        @Composable get() = navController
            .currentBackStackEntryAsState().value?.destination

    // UI logic
    fun navigate(destination: NiaNavigationDestination, route: String? = null) { /* ... */ }

     /* ... */
}

בדוגמה שלמעלה, הפרטים הבאים לגבי NiaAppState בולטים:

  • לא שורד יצירה מחדש של Activity: NiaAppState הוא remembered ב-Composition על ידי יצירה שלו באמצעות פונקציה שניתנת להגדרה rememberNiaAppState בהתאם למוסכמות השמות של Compose. אחרי שיוצרים מחדש את Activity, המופע הקודם אובד ונוצר מופע חדש עם כל התלויות שלו, שמתאים להגדרה החדשה של Activity שנוצר מחדש. יכול להיות שהתלויות האלה חדשות או שהן שוחזרו מההגדרה הקודמת. לדוגמה, rememberNavController() משמש ב-constructor‏ NiaAppState והוא מעביר את הפעולה אל rememberSaveable כדי לשמור על המצב במהלך יצירה מחדש של Activity.
  • כולל הפניות למקורות נתונים בהיקף ממשק המשתמש: אפשר להשתמש ב-NiaAppState בבטחה להפניות אל navigationController, Resources וסוגים דומים אחרים בהיקף מחזור החיים, כי הם חולקים את אותו היקף מחזור חיים.

בחירה בין ViewModel לבין מחלקה רגילה למאחסן מצב

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

לסיכום, בתרשים הבא מוצג המיקום של מאחסני מצב בפייפליין של יצירת מצב בממשק המשתמש:

הנתונים זורמים משכבת ייצור הנתונים לשכבת ממשק המשתמש
איור 6. מאחסני מצב בצינור הייצור של מצב ממשק המשתמש. החיצים מציינים את זרימת הנתונים.

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

אפשר לשלב בין מאחסני מצב

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

  • מאחסן מצבים של לוגיקת ממשק משתמש יכול להיות תלוי במאחסן מצבים אחר של לוגיקת ממשק משתמש.
  • מחזיק מצב ברמת המסך יכול להיות תלוי במחזיק מצב של לוגיקת ממשק המשתמש.

בקטע הקוד הבא אפשר לראות איך Compose's DrawerState תלויה במחזיק מצב פנימי אחר, SwipeableState, ואיך מחזיק מצב של לוגיקת ממשק המשתמש של אפליקציה יכול להיות תלוי ב-DrawerState:

@Stable
class DrawerState(/* ... */) {
  internal val swipeableState = SwipeableState(/* ... */)
  // ...
}

@Stable
class MyAppState(
  private val drawerState: DrawerState,
  private val navController: NavHostController
) { /* ... */ }

@Composable
fun rememberMyAppState(
  drawerState: DrawerState = rememberDrawerState(DrawerValue.Closed),
  navController: NavHostController = rememberNavController()
): MyAppState = remember(drawerState, navController) {
  MyAppState(drawerState, navController)
}

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

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

class MyScreenViewModel(/* ... */) {
  val uiState: StateFlow<MyScreenUiState> = /* ... */
  fun doSomething() { /* ... */ }
  fun doAnotherThing() { /* ... */ }
  // ...
}

@Stable
class MyScreenState(
  // DO NOT pass a ViewModel instance to a plain state holder class
  // private val viewModel: MyScreenViewModel,

  // Instead, pass only what it needs as a dependency
  private val someState: StateFlow<SomeState>,
  private val doSomething: () -> Unit,

  // Other UI-scoped types
  private val scaffoldState: ScaffoldState
) {
  /* ... */
}

@Composable
fun rememberMyScreenState(
  someState: StateFlow<SomeState>,
  doSomething: () -> Unit,
  scaffoldState: ScaffoldState = rememberScaffoldState()
): MyScreenState = remember(someState, doSomething, scaffoldState) {
  MyScreenState(someState, doSomething, scaffoldState)
}

@Composable
fun MyScreen(
  modifier: Modifier = Modifier,
  viewModel: MyScreenViewModel = viewModel(),
  state: MyScreenState = rememberMyScreenState(
    someState = viewModel.uiState.map { it.toSomeState() },
    doSomething = viewModel::doSomething
  ),
  // ...
) {
  /* ... */
}

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

ממשק המשתמש תלוי במאחסן המצב של לוגיקת ממשק המשתמש ובמאחסן המצב ברמת המסך
איור 7. ממשק המשתמש בהתאם למאחסני מצבים שונים. החיצים מציינים יחסי תלות.

דוגמאות

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