ב-Compose, ממשק המשתמש לא ניתן לשינוי – אי אפשר לעדכן אותו אחרי שהוא נוצר. מה שאתם יכולים לשלוט בו הוא המצב של ממשק המשתמש. בכל פעם שהמצב של ממשק המשתמש משתנה, Compose יוצר מחדש את החלקים של עץ ממשק המשתמש שהשתנו. רכיבי Composable יכולים לקבל מצב ולחשוף אירועים. לדוגמה, TextField מקבל ערך וחושף קריאה חוזרת (callback) onValueChange שמבקשת מהגורם המטפל בקריאה החוזרת לשנות את הערך.
var name by remember { mutableStateOf("") } OutlinedTextField( value = name, onValueChange = { name = it }, label = { Text("Name") } )
מכיוון שרכיבים קומפוזביליים מקבלים מצב וחושפים אירועים, דפוס זרימת הנתונים החד-כיוונית מתאים מאוד ל-Jetpack Compose. המדריך הזה מתמקד בדרכים להטמיע את דפוס זרימת הנתונים החד-כיוונית ב-Compose, להטמיע אירועים ומאחסני מצב ולעבוד עם ViewModels ב-Compose.
זרימת נתונים חד-כיוונית
זרימת נתונים חד-כיוונית (UDF) היא תבנית עיצוב שבה המצב זורם למטה והאירועים זורמים למעלה. כשמשתמשים בזרימת נתונים חד-כיוונית, אפשר להפריד בין רכיבים קומפוזביליים שמציגים את המצב בממשק המשתמש לבין החלקים באפליקציה שמאחסנים את המצב ומשנים אותו.
לולאת העדכון של ממשק המשתמש באפליקציה שמשתמשת בזרימת נתונים חד-כיוונית נראית כך:
- אירוע: חלק מממשק המשתמש יוצר אירוע ומעביר אותו למעלה, למשל לחיצה על כפתור שמועברת ל-ViewModel לטיפול, או אירוע שמועבר משכבות אחרות של האפליקציה, כמו ציון שסשן המשתמש פג.
- עדכון המצב: גורם מטפל באירועים עשוי לשנות את המצב.
- מצב התצוגה: מאחסן המצב מעביר את המצב, וממשק המשתמש מציג אותו.
כשמשתמשים ב-Jetpack Compose, יש כמה יתרונות לשימוש בתבנית הזו:
- יכולת בדיקה: הפרדת המצב מממשק המשתמש שמציג אותו מאפשרת לבדוק את שניהם בנפרד.
- אנקפסולציה של ערך דינמי: מכיוון שאפשר לעדכן את הערך הדינמי רק במקום אחד ויש רק מקור אחד של מידע מהימן לערך הדינמי של פונקציה קומפוזבילית, פחות סביר שתצרו באגים בגלל ערכים דינמיים לא עקביים.
- עקביות בממשק המשתמש: כל העדכונים של המצב משתקפים מיד בממשק המשתמש באמצעות מאחסני מצב שניתן לצפות בהם, כמו
StateFlowאוLiveData.
זרימת נתונים חד-כיוונית ב-Jetpack Compose
רכיבי Composable פועלים על סמך מצב ואירועים. לדוגמה, TextField מתעדכן רק כשהפרמטר value שלו מתעדכן, והוא חושף קריאה חוזרת (callback) מסוג onValueChange – אירוע שמבקש לשנות את הערך לערך חדש. מערכת פיתוח נייטיב מגדירה את האובייקט State כמחזיק ערך, ושינויים בערך המצב מפעילים קומפוזיציה מחדש. אפשר לשמור את המצב ב-remember { mutableStateOf(value) } או ב-rememberSaveable { mutableStateOf(value) }, בהתאם למשך הזמן שבו צריך לזכור את הערך.
הסוג של הערך של הפונקציה ה-composable TextField הוא String, אז הוא יכול להגיע מכל מקום – מערך בתוך הקוד, מ-ViewModel או מהפונקציה ה-composable ההורה. לא חייבים להחזיק אותו באובייקט State, אבל צריך לעדכן את הערך כשמתבצעת קריאה ל-onValueChange.
- הפונקציה
mutableStateOf(value)יוצרתMutableState, שהוא סוג שניתן לצפייה בפיתוח נייטיב. כל שינוי בערך שלו מתזמן הרכבה מחדש של כל פונקציה קומפוזבילית שקוראת את הערך הזה. - הפונקציה
rememberשומרת אובייקטים בקומפוזיציה, ומוחקת את האובייקט כשהפונקציה ה-composable שקראה ל-rememberמוסרת מהקומפוזיציה. - הפונקציה
rememberSaveableשומרת את המצב ב-Bundleכדי לשמור עליו גם כשמשנים את ההגדרות.
הגדרת פרמטרים של פונקציות קומפוזביליות
כשמגדירים את הפרמטרים של המצב של פונקציה קומפוזבילית, חשוב לשאול את עצמכם את השאלות הבאות:
- עד כמה ה-composable גמיש או ניתן לשימוש חוזר?
- איך הפרמטרים של המצב משפיעים על הביצועים של ה-composable הזה?
כדי לקדם את ההפרדה והשימוש החוזר, כל פונקציה קומפוזבילית צריכה להכיל כמה שפחות מידע. לדוגמה, כשיוצרים רכיב 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 לא השתנו.
חשוב לשקול היטב את מספר הפרמטרים שאתם מעבירים. פונקציה עם יותר מדי פרמטרים פחות נוחה לשימוש, ולכן במקרה כזה עדיף לקבץ אותם במחלקה.
אירועים ב-Compose
כל קלט באפליקציה צריך להיות מיוצג כאירוע: לחיצות, שינויים בטקסט ואפילו טיימרים או עדכונים אחרים. האירועים האלה משנים את המצב של ממשק המשתמש, ולכן ה-ViewModel צריך לטפל בהם ולעדכן את המצב של ממשק המשתמש.
שכבת ממשק המשתמש לא אמורה לשנות את המצב מחוץ לגורם המטפל באירועים, כי זה עלול לגרום לחוסר עקביות ולבאגים באפליקציה.
עדיף להעביר ערכים שלא ניתנים לשינוי עבור למבדות של מצב ושל גורם מטפל באירועים. לגישה הזו יש כמה יתרונות:
- אתם משפרים את היכולת להשתמש מחדש בנתונים.
- אתם מוודאים שממשק המשתמש לא משנה את הערך של המצב באופן ישיר.
- כך נמנעות בעיות של בו-זמניות, כי אתם מוודאים שהמצב לא משתנה משרשור אחר.
- במקרים רבים, זה מפחית את מורכבות הקוד.
לדוגמה, אפשר לקרוא לפונקציה קומפוזבילית שמקבלת String ולמבדה כפרמטרים מהרבה הקשרים, והיא ניתנת לשימוש חוזר במידה רבה. נניח שסרגל האפליקציה העליון באפליקציה תמיד מציג טקסט ויש בו הכפתור "הקודם". אפשר להגדיר פונקציית 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מטפל באירועים שמגיעים מממשק המשתמש או משכבות אחרות של האפליקציה, ומעדכן את מאחסן המצב על סמך האירועים.
לדוגמה, כשמטמיעים מסך כניסה, לחיצה על הלחצן כניסה אמורה לגרום לאפליקציה להציג סמל טעינה וקריאה לרשת. אם ההתחברות הצליחה, האפליקציה עוברת למסך אחר. אם יש שגיאה, האפליקציה מציגה Snackbar. כך אפשר ליצור מודל של מצב המסך ושל האירוע:
למסך יש ארבעה מצבים:
- לא מחובר: כשהמשתמש עדיין לא נכנס לחשבון.
- בתהליך: כשהאפליקציה מנסה להכניס את המשתמש באמצעות שיחת רשת.
- שגיאה: אם אירעה שגיאה במהלך הכניסה.
- מחובר: כשהמשתמש מחובר לחשבון.
אפשר ליצור מודל של המצבים האלה כמחלקה אטומה. ה-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 פיתוח נייטיב
- שמירת מצב ממשק המשתמש בפיתוח נייטיב
- טיפול בקלט של משתמשים