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

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

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

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

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

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

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

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

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

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

  • מצב ממשק המשתמש במסך הוא מה שצריך להציג במסך. לדוגמה, כיתה אחת (NewsUiState) יכולה להכיל את הכתבות החדשותיות ופרטים נוספים שנדרשים כדי לעבד את ממשק המשתמש. המצב הזה מחובר בדרך כלל לשכבות אחרות ההיררכיה כי היא מכילה נתוני אפליקציות.
  • מצב רכיב בממשק המשתמש מתייחס למאפיינים שמרכיבים את רכיבי ממשק המשתמש להשפיע על אופן ההצגה שלהם. רכיב בממשק המשתמש עשוי להיות מוצג או מוסתר, להיות בעלי גופן, גודל גופן או צבע מסוים מסוימים. ב-Android Views, התצוגה שמנהל את המצב הזה בעצמו כי הוא שומר על מצב, כך שהוא חושף שיטות לשנות את המצב או לשלוח שאילתות לגביו. לדוגמה: get set methods של המחלקה TextView לטקסט. ב-Jetpack כתוב, המצב הוא חיצוני לתוכן הקומפוזבילי, ואפשר אפילו להעלות אותו לא בסביבה המיידית של התוכן הקומפוזבילי עם תוכן קומפוזבילי פונקציה או בעל מצב. דוגמה לזה הוא ScaffoldState עבור תוכן קומפוזבילי Scaffold.

לוגיקה

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

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

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

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

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

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

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

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

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

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

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

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

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

    @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)
                }
            }
        }
    }
    

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

הנתונים עוברים משכבת הפקת הנתונים אל ממשק המשתמש
איור 3: יישום הלוגיקה בשכבת ממשק המשתמש

מחזיקי מדינות וחובותיהם

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

זה מספק את היתרונות הבאים:

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

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

סוגי בעלי המדינות

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

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

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

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

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

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

לדוגמה, נבחן את יעד הניווט של המחבר בקובץ Android" app:

אפליקציית Now ב-Android מדגימה איך יעד ניווט שמייצג פונקציה ראשית של אפליקציה צריך להיות
בעל מצב לוגיקה עסקי ייחודי משלו.
איור 4: האפליקציה 'עכשיו ב-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 כולל את המאפיינים שתוארו קודם לכן:

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

ל-ViewModel, לבעלי מצב הלוגיקה העסקית

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

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

לוגיקת ממשק המשתמש ומחזיק המצב שלו

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

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

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

אפשר לראות זאת בדוגמה הבאה עכשיו בדוגמה ל-Android:

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

בדוגמה של 'עכשיו ב-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 ב- את היצירה המורכבת על ידי יצירתה באמצעות פונקציה קומפוזבילית rememberNiaAppState בהתאם למוסכמות מתן השמות של 'פיתוח נייטיב'. אחרי יצירה מחדש של Activity, המופע הקודם אבד ונוצר מכונה חדשה עם כל של יחסי התלות שמועברים, שמתאימים לתצורה החדשה של יצר מחדש את Activity. יכול להיות שיחסי התלות האלה חדשים או ישוחזרו את ההגדרה הקודמת. לדוגמה, rememberNavController() נמצא בשימוש ב- ה-constructor של NiaAppState והוא מעניק את ההרשאה ל-rememberSaveable כדי לשמר את המצב בפעילות המשותפת Activity.
  • כולל הפניות למקורות נתונים בהיקף ממשק המשתמש: הפניות אל navigationController, Resources וסוגים דומים של מחזור חיים ניתן להחזיק בבטחה ב-NiaAppState כי הם חולקים אותו היקף של מחזור חיים.

בחירה בין ViewModel למחלקה פשוטה, לבעלי מדינה

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

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

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

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

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

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

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

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