ב-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 שמציגות מצב בממשק המשתמש לבין החלקים באפליקציה שמאחסנים ומשנים את המצב.
לולאת עדכון ממשק המשתמש של אפליקציה שמשתמשת בזרימת נתונים חד-כיוונית נראית כך:
- אירוע: חלק מממשק המשתמש יוצר אירוע ומעביר אותו כלפי מעלה, כמו לחיצה על לחצן שמועברת אל ViewModel כדי לטפל בה, או אירוע שמועבר משכבות אחרות באפליקציה, כמו אינדיקציה לכך שהסשן של המשתמש הסתיים.
- עדכון המצב: יכול להיות שגורם מטפל באירועים ישנה את המצב.
- מצב התצוגה: מחזיק המצב מעביר את המצב, וממשק המשתמש מציג אותו.
שימוש בתבנית הזו כשמשתמשים ב-Jetpack Compose מספק כמה יתרונות:
- יכולת בדיקה: הפרדת המצב מממשק המשתמש שמציג אותו מקלה על הבדיקה של שניהם בנפרד.
- אנקפסולציית מצב: מכיוון שאפשר לעדכן את המצב רק במקום אחד, ויש רק מקור אחד לאמת לגבי המצב של פונקציה קומפוזבילית, הסיכוי ליצירת באגים בגלל מצבים לא עקביים נמוך יותר.
- עקביות בממשק המשתמש: כל עדכוני המצב משתקפים מיד בממשק המשתמש באמצעות שימוש במחזיקי מצב שניתן לצפות בהם, כמו
StateFlowאוLiveData.
זרימת נתונים חד-כיוונית ב-Jetpack Compose
רכיבי Composables פועלים על סמך מצב ואירועים. לדוגמה, TextField מתעדכן רק כשהפרמטר value שלו מתעדכן, והוא חושף קריאה חוזרת (callback) של onValueChange – אירוע שמבקש לשנות את הערך לערך חדש. ב-Compose, אובייקט State מוגדר כמחזיק ערך, ושינויים בערך המצב מפעילים קומפוזיציה מחדש. אפשר לשמור את המצב ב-remember { mutableStateOf(value) } או ב-rememberSaveable { mutableStateOf(value), בהתאם למשך הזמן שבו צריך לזכור את הערך.
הסוג של הערך של רכיב ה-Composable TextField הוא String, ולכן הוא יכול להגיע מכל מקום – מערך שמוגדר בהארדקוד, מ-ViewModel או מ-Composable אב. לא חייבים להחזיק אותו באובייקט 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 צריך לטפל בהם ולעדכן את מצב ממשק המשתמש.
שכבת ממשק המשתמש לא אמורה לשנות את ערך דינמי שלה מחוץ ל-גורם מטפל באירועים, כי זה עלול לגרום לחוסר עקביות ולבאגים באפליקציה.
מומלץ להעביר ערכים שלא ניתן לשנות עבור ערך דינמי ופונקציות למדה של גורם מטפל באירועים. היתרונות של הגישה הזו:
- משפרים את האפשרות לשימוש חוזר.
- מוודאים שממשק המשתמש לא משנה את ערך המצב ישירות.
- אתם נמנעים מבעיות של בו-זמניות כי אתם מוודאים שהערך הדינמי לא משתנה מ-Thread אחר.
- לעתים קרובות, אתם מצמצמים את מורכבות הקוד.
לדוגמה, אפשר לקרוא לרכיב שאפשר להרכיב ממנו ממשקים שמקבל פרמטרים של String ו-lambda מהרבה הקשרים, והוא ניתן לשימוש חוזר. נניח שסרגל האפליקציות העליון באפליקציה שלכם תמיד מציג טקסט ויש בו לחצן חזרה. אפשר להגדיר MyAppTopAppBar פונקציה קומפוזבילית כללית יותר
שמקבלת את הטקסט ואת handler של הכפתור "הקודם" כפרמטרים:
@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 זמין במקורות המידע הבאים:
דוגמאות
מומלץ בשבילך
- הערה: טקסט הקישור מוצג כש-JavaScript מושבת
- מצב ו-Jetpack פיתוח נייטיב
- שמירת מצב ממשק המשתמש בפיתוח נייטיב
- טיפול בקלט של משתמשים