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

ב-Compose, ממשק המשתמש הוא קבוע – אי אפשר לעדכן אותו אחרי שהוא מוצג. מה שאתם יכולים לשלוט בו הוא המצב של ממשק המשתמש. בכל פעם שהמצב של ממשק המשתמש משתנה, Compose יוצר מחדש את החלקים בעץ ממשק המשתמש שהשתנו. פונקציות composable יכולות לקבל מצב ולחשוף אירועים – לדוגמה, TextField מקבל ערך וחושף קריאה חוזרת onValueChange שמבקשת ממטפל הקריאה החוזרת לשנות את הערך.

var name by remember { mutableStateOf("") }
OutlinedTextField(
    value = name,
    onValueChange = { name = it },
    label = { Text("Name") }
)

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

זרימת נתונים חד-כיוונית

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

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

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

שימוש בתבנית הזו ב-Jetpack Compose מספק כמה יתרונות:

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

זרימת נתונים חד-כיוונית ב-Jetpack פיתוח נייטיב

רכיבי Composables פועלים על סמך מצב ואירועים. לדוגמה, TextField מתעדכן רק כשהפרמטר value שלו מתעדכן, והוא חושף קריאה חוזרת (callback) של onValueChange – אירוע שמבקש לשנות את הערך לערך חדש. ב-Compose, אובייקט State מוגדר כמחזיק ערך, ושינויים בערך המצב מפעילים קומפוזיציה מחדש. אפשר לשמור את המצב ב-remember { mutableStateOf(value) } או ב-rememberSaveable { mutableStateOf(value), בהתאם למשך הזמן שבו צריך לזכור את הערך.

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

הגדרת פרמטרים שאפשר להרכיב

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

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

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

@Composable
fun Header(title: String, subtitle: String) {
    // Recomposes when title or subtitle have changed.
}

@Composable
fun Header(news: News) {
    // Recomposes when a new instance of News is passed in.
}

לפעמים שימוש בפרמטרים נפרדים גם משפר את הביצועים – לדוגמה, אם News מכיל יותר מידע מאשר רק title ו-subtitle, בכל פעם שמועבר מופע חדש של News אל Header(news), הרכיב הניתן להרכבה מחדש יורכב מחדש, גם אם title ו-subtitle לא השתנו.

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

אירועים בכתיבה

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

שכבת ממשק המשתמש לא צריכה לשנות את המצב שלה מחוץ ל-event handler, כי זה עלול לגרום לחוסר עקביות ולבאגים באפליקציה.

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

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

לדוגמה, אפשר לקרוא לרכיב שאפשר להרכיב שמקבל String ו-lambda כפרמטרים מהרבה הקשרים, והוא ניתן לשימוש חוזר. נניח שסרגל האפליקציות העליון באפליקציה שלכם תמיד מציג טקסט ויש בו לחצן חזרה. אפשר להגדיר MyAppTopAppBar composable כללי יותר שמקבל את הטקסט ואת הטיפול בכפתור 'הקודם' כפרמטרים:

@Composable
fun MyAppTopAppBar(topAppBarText: String, onBackPressed: () -> Unit) {
    TopAppBar(
        title = {
            Text(
                text = topAppBarText,
                textAlign = TextAlign.Center,
                modifier = Modifier
                    .fillMaxSize()
                    .wrapContentSize(Alignment.Center)
            )
        },
        navigationIcon = {
            IconButton(onClick = onBackPressed) {
                Icon(
                    Icons.AutoMirrored.Filled.ArrowBack,
                    contentDescription = localizedString
                )
            }
        },
        // ...
    )
}

דוגמה ל-ViewModels, למצבים ולאירועים

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

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

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

יש ארבעה מצבים למסך:

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

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

class MyViewModel : ViewModel() {
    private val _uiState = mutableStateOf<UiState>(UiState.SignedOut)
    val uiState: State<UiState>
        get() = _uiState

    // ...
}

בנוסף ל-API‏ mutableStateOf,‏ Compose מספקת תוספים ל-LiveData,‏ Flow ו-Observable כדי להירשם כמאזין ולייצג את הערך כמצב.

class MyViewModel : ViewModel() {
    private val _uiState = MutableLiveData<UiState>(UiState.SignedOut)
    val uiState: LiveData<UiState>
        get() = _uiState

    // ...
}

@Composable
fun MyComposable(viewModel: MyViewModel) {
    val uiState = viewModel.uiState.observeAsState()
    // ...
}

מידע נוסף

מידע נוסף על ארכיטקטורה ב-Jetpack Compose זמין במקורות המידע הבאים:

טעימות