מצב באפליקציה הוא כל ערך שיכול להשתנות לאורך זמן. זו הגדרה רחבה מאוד שכוללת כל דבר, החל ממסד נתונים של Room ועד למשתנה בכיתה.
כל אפליקציות Android מציגות את הסטטוס למשתמש. דוגמאות למצב באפליקציות ל-Android:
- סנאקבר שמוצג כשאי אפשר להתחבר לרשת.
- פוסט בבלוג והתגובות שקשורות אליו.
- אנימציות של גלים בכפתורים שמופעלות כשמשתמש לוחץ עליהם.
- מדבקות שמשתמש יכול לצייר מעל תמונה.
Jetpack Compose עוזר לכם להגדיר במפורש איפה ואיך אתם מאחסנים ומשתמשים ב-state באפליקציית Android. המדריך הזה מתמקד בקשר בין state לבין composables, ובממשקי ה-API ש-Jetpack Compose מציע כדי לעבוד עם state בצורה קלה יותר.
מצב והרכב
Compose הוא הצהרתי, ולכן הדרך היחידה לעדכן אותו היא לקרוא לאותו רכיב שאפשר להרכיב עם ארגומנטים חדשים. הארגומנטים האלה מייצגים את מצב ממשק המשתמש. בכל פעם שמצב מתעדכן, מתבצעת קומפוזיציה מחדש. כתוצאה מכך, דברים כמו TextField לא מתעדכנים אוטומטית כמו בתצוגות מבוססות XML אימפרטיבי. כדי שרכיב ה-Composable יתעדכן בהתאם, צריך לציין לו באופן מפורש את המצב החדש.
@Composable private fun HelloContent() { Column(modifier = Modifier.padding(16.dp)) { Text( text = "Hello!", modifier = Modifier.padding(bottom = 8.dp), style = MaterialTheme.typography.bodyMedium ) OutlinedTextField( value = "", onValueChange = { }, label = { Text("Name") } ) } }
אם תריצו את הפקודה הזו ותנסו להזין טקסט, תראו שלא קורה כלום. הסיבה לכך היא ש-TextField לא מתעדכן בעצמו – הוא מתעדכן כשפרמטר value שלו משתנה. הסיבה לכך היא האופן שבו קומפוזיציה וקומפוזיציה מחדש פועלות ב-Compose.
מידע נוסף על הרכבה ראשונית והרכבה מחדש זמין במאמר Thinking in Compose.
מצב ברכיבים קומפוזביליים
פונקציות שאפשר להרכיב יכולות להשתמש ב-API remember כדי לאחסן אובייקט בזיכרון. ערך שמחושב על ידי remember מאוחסן בקומפוזיציה במהלך הקומפוזיציה הראשונית, והערך המאוחסן מוחזר במהלך הקומפוזיציה מחדש.
אפשר להשתמש ב-remember כדי לאחסן אובייקטים שניתנים לשינוי ואובייקטים שלא ניתנים לשינוי.
mutableStateOf
יוצרת אובייקט ניתן לצפייה MutableState<T>, שהוא סוג ניתן לצפייה שמשולב עם זמן הריצה של Compose.
interface MutableState<T> : State<T> {
override var value: T
}
כל שינוי ב-value מתזמן קומפוזיציה מחדש של כל הפונקציות הקומפוזביליות שקוראות את value.
יש שלוש דרכים להצהיר על אובייקט MutableState בקומפוזיציה:
val mutableState = remember { mutableStateOf(default) }var value by remember { mutableStateOf(default) }val (value, setValue) = remember { mutableStateOf(default) }
ההצהרות האלה שוות ערך, והן מסופקות כקיצור תחבירי לשימושים שונים במצב. כדאי לבחור את האפשרות שיוצרת את הקוד הכי קל לקריאה בקומפוזיציה שאתם כותבים.
התחביר של by delegate מחייב את הייבואים הבאים:
import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue
אפשר להשתמש בערך שנשמר כפרמטר לרכיבים אחרים שניתנים להרכבה, או אפילו כהיגיון בהצהרות כדי לשנות את הרכיבים שמוצגים. לדוגמה, אם לא רוצים להציג את הברכה אם השם ריק, משתמשים במצב בהצהרה if:
@Composable fun HelloContent() { Column(modifier = Modifier.padding(16.dp)) { var name by remember { mutableStateOf("") } if (name.isNotEmpty()) { Text( text = "Hello, $name!", modifier = Modifier.padding(bottom = 8.dp), style = MaterialTheme.typography.bodyMedium ) } OutlinedTextField( value = name, onValueChange = { name = it }, label = { Text("Name") } ) } }
הפונקציה remember עוזרת לשמור על המצב בין הרכבות מחדש, אבל המצב לא נשמר בין שינויים בהגדרות. לצורך זה, צריך להשתמש ב-rememberSaveable. rememberSaveable שומר באופן אוטומטי כל ערך שאפשר לשמור ב-Bundle. לערכים אחרים, אפשר להעביר אובייקט מותאם אישית של שמירה.
סוגים נתמכים אחרים של מצב
ב-Compose לא צריך להשתמש ב-MutableState<T> כדי לשמור את המצב, כי הוא תומך בסוגים אחרים של אובייקטים שאפשר לעקוב אחריהם. לפני שקוראים סוג אחר של נתונים שניתנים לצפייה ב-Compose, צריך להמיר אותם ל-State<T> כדי שפונקציות composable יוכלו לבצע קומפוזיציה מחדש באופן אוטומטי כשהמצב משתנה.
Compose כולל פונקציות ליצירת State<T> מסוגים נפוצים של אובייקטים שאפשר לצפות בהם, שמשמשים באפליקציות ל-Android. לפני שמשתמשים בשילובים האלה, צריך להוסיף את הארטיפקטים המתאימים, כמו שמתואר בהמשך:
Flow:collectAsStateWithLifecycle()
collectAsStateWithLifecycle()אוסף ערכים מ-Flowבאופן שמודע למחזור החיים, וכך מאפשר לאפליקציה לחסוך במשאבים. הוא מייצג את הערך האחרון שהונפק מ-ComposeState. מומלץ להשתמש ב-API הזה כדי לאסוף נתונים על תהליכים באפליקציות ל-Android.התלות הבאה נדרשת בקובץ
build.gradle(היא צריכה להיות בגרסה 2.6.0-beta01 ומעלה):
Kotlin
dependencies {
...
implementation("androidx.lifecycle:lifecycle-runtime-compose:2.9.4")
}
Groovy
dependencies {
...
implementation "androidx.lifecycle:lifecycle-runtime-compose:2.9.4"
}
-
הפונקציה
collectAsStateדומה לפונקציהcollectAsStateWithLifecycle, כי היא גם אוספת ערכים מ-Flowומשנה אותם ל-ComposeState.כדי להשתמש בקוד שמתאים לכל הפלטפורמות, צריך להשתמש ב-
collectAsStateבמקום ב-collectAsStateWithLifecycle, שמתאים רק ל-Android.לא נדרשים יחסי תלות נוספים עבור
collectAsState, כי הוא זמין ב-compose-runtime. -
observeAsState()מתחיל לעקוב אחריLiveDataומציג את הערכים שלו באמצעותState.התלות הבאה נדרשת בקובץ
build.gradle:
Kotlin
dependencies {
...
implementation("androidx.compose.runtime:runtime-livedata:1.9.3")
}
Groovy
dependencies {
...
implementation "androidx.compose.runtime:runtime-livedata:1.9.3"
}
-
subscribeAsState()הן פונקציות הרחבה שממירות את הזרמים הריאקטיביים של RxJava2 (למשלSingle, Observable,Completable) ל-Stateשל Compose.התלות הבאה נדרשת בקובץ
build.gradle:
Kotlin
dependencies {
...
implementation("androidx.compose.runtime:runtime-rxjava2:1.9.3")
}
Groovy
dependencies {
...
implementation "androidx.compose.runtime:runtime-rxjava2:1.9.3"
}
-
subscribeAsState()הן פונקציות הרחבה שממירות את הזרמים הריאקטיביים של RxJava3 (למשלSingle, Observable,Completable) ל-Stateשל Compose.התלות הבאה נדרשת בקובץ
build.gradle:
Kotlin
dependencies {
...
implementation("androidx.compose.runtime:runtime-rxjava3:1.9.3")
}
Groovy
dependencies {
...
implementation "androidx.compose.runtime:runtime-rxjava3:1.9.3"
}
עם שמירת מצב לעומת ללא שמירת מצב
קומפוזבל שמשתמש ב-remember כדי לאחסן אובייקט יוצר מצב פנימי, ולכן הקומפוזבל הוא stateful. HelloContent היא דוגמה לרכיב שאפשר להוסיף לו מצב כי הוא מחזיק את המצב name ומשנה אותו באופן פנימי. האפשרות הזו יכולה להיות שימושית במצבים שבהם המתקשר לא צריך לשלוט במצב ויכול להשתמש בו בלי לנהל את המצב בעצמו. עם זאת, קומפוזבלים עם מצב פנימי נוטים להיות פחות ניתנים לשימוש חוזר וקשים יותר לבדיקה.
קומפוזבילי חסר מצב הוא קומפוזבילי שלא מחזיק מצב. דרך קלה להשיג מצב חסר סטטוס היא באמצעות העברת סטטוס.
כשמפתחים קומפוזיציות לשימוש חוזר, לרוב רוצים לחשוף גם גרסה עם מצב וגם גרסה ללא מצב של אותה קומפוזיציה. הגרסה עם שמירת מצב נוחה למבצעי קריאה שלא אכפת להם מהמצב, והגרסה ללא שמירת מצב נחוצה למבצעי קריאה שצריכים לשלוט במצב או להעביר אותו.
העלאת הרמה של מצב (state hoisting)
העברת מצב (State hoisting) ב-Compose היא דפוס של העברת מצב לקומפוזבילי שקורא לפונקציה כדי להפוך את הקומפוזבילי ללא מצב (stateless). הדפוס הכללי להעברת מצב ב-Jetpack Compose הוא להחליף את משתנה המצב בשני פרמטרים:
-
value: T: הערך הנוכחי שיוצג -
onValueChange: (T) -> Unit: אירוע שמבקש לשנות את הערך, כאשרTהוא הערך החדש המוצע
עם זאת, אתם לא מוגבלים ל-onValueChange. אם יש אירועים ספציפיים יותר שמתאימים לקומפוזבל, צריך להגדיר אותם באמצעות ביטויי למבדה.
למצב שמועבר בדרך הזו יש כמה מאפיינים חשובים:
- מקור מרוכז אחד: העברת המצב במקום שכפול שלו מבטיחה שיש רק מקור מרוכז אחד. כך אפשר להימנע מבאגים.
- עטוף: רק קומפוזיציות עם מצב יכולות לשנות את המצב שלהן. הוא פנימי לחלוטין.
- ניתן לשיתוף: אפשר לשתף מצב מועבר עם מספר רכיבי Composable. אם רוצים לקרוא את
nameבקומפוזיציה אחרת, אפשר לעשות את זה באמצעות hoisting. - ניתן ליירט: המתקשרים לפונקציות הניתנות להרכבה ללא מצב יכולים להחליט להתעלם מאירועים או לשנות אותם לפני שינוי המצב.
- מנותק: המצב של רכיבי ה-Composable חסרי המצב יכול להיות מאוחסן בכל מקום. לדוגמה, עכשיו אפשר להעביר את
nameאלViewModel.
בדוגמה, מחלצים את name ואת onValueChange מתוך HelloContent ומעבירים אותם למעלה בעץ אל רכיב HelloScreen שאפשר להרכיב שקורא ל-HelloContent.
@Composable fun HelloScreen() { var name by rememberSaveable { mutableStateOf("") } HelloContent(name = name, onNameChange = { name = it }) } @Composable fun HelloContent(name: String, onNameChange: (String) -> Unit) { Column(modifier = Modifier.padding(16.dp)) { Text( text = "Hello, $name", modifier = Modifier.padding(bottom = 8.dp), style = MaterialTheme.typography.bodyMedium ) OutlinedTextField(value = name, onValueChange = onNameChange, label = { Text("Name") }) } }
כשמעבירים את הסטייט מחוץ ל-HelloContent, קל יותר להבין את הקומפוזיציה, להשתמש בה מחדש במצבים שונים ולבדוק אותה. HelloContent לא תלוי באופן שבו המצב שלו מאוחסן. המשמעות של הפרדה היא שאם משנים או מחליפים את HelloScreen, לא צריך לשנות את האופן שבו HelloContent מיושם.
הדפוס שבו המצב יורד והאירועים עולים נקרא זרימת נתונים חד-כיוונית. במקרה הזה, המצב יורד מ-HelloScreen
ל-HelloContent והאירועים עולים מ-HelloContent ל-HelloScreen. על ידי שימוש בזרימת נתונים חד-כיוונית, אפשר להפריד בין רכיבי Composable שמציגים מצב בממשק המשתמש לבין החלקים באפליקציה שמאחסנים ומשנים את המצב.
מידע נוסף זמין בדף איפה כדאי להעביר את מצב הרכיב.
שחזור מצב בפיתוח נייטיב
התנהגות ה-API של rememberSaveable דומה לזו של remember כי הוא שומר על המצב בין הרכבות מחדש, וגם בין יצירה מחדש של פעילות או תהליך באמצעות מנגנון שמירת מצב המופע. לדוגמה, זה קורה כשמסובבים את המסך.
דרכים לאחסון מצב
כל סוגי הנתונים שמוסיפים ל-Bundle נשמרים אוטומטית. אם אתם רוצים לשמור משהו שלא ניתן להוסיף ל-Bundle, יש כמה אפשרויות.
Parcelize
הפתרון הפשוט ביותר הוא להוסיף את ההערה @Parcelize לאובייקט. האובייקט הופך לאובייקט שניתן להעברה, ואפשר לאגד אותו. לדוגמה, הקוד הזה יוצר סוג נתונים City שניתן להעברה בחבילה ושומר אותו במצב.
@Parcelize data class City(val name: String, val country: String) : Parcelable @Composable fun CityScreen() { var selectedCity = rememberSaveable { mutableStateOf(City("Madrid", "Spain")) } }
MapSaver
אם מסיבה כלשהי @Parcelize לא מתאים, אפשר להשתמש ב-mapSaver כדי להגדיר כלל משלכם להמרת אובייקט לקבוצת ערכים שהמערכת יכולה לשמור ב-Bundle.
data class City(val name: String, val country: String) val CitySaver = run { val nameKey = "Name" val countryKey = "Country" mapSaver( save = { mapOf(nameKey to it.name, countryKey to it.country) }, restore = { City(it[nameKey] as String, it[countryKey] as String) } ) } @Composable fun CityScreen() { var selectedCity = rememberSaveable(stateSaver = CitySaver) { mutableStateOf(City("Madrid", "Spain")) } }
ListSaver
כדי להימנע מהצורך להגדיר את המפתחות למיפוי, אפשר גם להשתמש ב-listSaver ולהשתמש באינדקסים שלו כמפתחות:
data class City(val name: String, val country: String) val CitySaver = listSaver<City, Any>( save = { listOf(it.name, it.country) }, restore = { City(it[0] as String, it[1] as String) } ) @Composable fun CityScreen() { var selectedCity = rememberSaveable(stateSaver = CitySaver) { mutableStateOf(City("Madrid", "Spain")) } }
מאחסני מצב ב-Compose
אפשר לנהל העברת מצב פשוטה בפונקציות הניתנות להרכבה עצמן. עם זאת, אם כמות הנתונים שצריך לעקוב אחריהם גדלה, או אם יש צורך בלוגיקה לביצוע בפונקציות שניתנות להרכבה, מומלץ להעביר את האחריות על הלוגיקה והנתונים למחלקות אחרות: מחזיקי מצב.
מידע נוסף זמין במאמרי העזרה בנושא state hoisting ב-Compose או בדף State holders and UI State במדריך לארכיטקטורה.
הפעלה מחדש של חישובי הזיכרון כשמשנים מקשים
remember API משמש לעיתים קרובות יחד עם MutableState:
var name by remember { mutableStateOf("") }
במקרה הזה, השימוש בפונקציה remember מאפשר לערך MutableState לשרוד את ההרכבות מחדש.
באופן כללי, הפונקציה remember מקבלת פרמטר למדה calculation. כשמריצים את הפונקציה remember
בפעם הראשונה, היא מפעילה את פונקציית ה-lambda calculation ושומרת את התוצאה שלה. במהלך
ההרכבה מחדש, הפונקציה remember מחזירה את הערך שאוחסן לאחרונה.
בנוסף לשמירת מצב במטמון, אפשר להשתמש ב-remember כדי לאחסן בהרכבה אובייקט או תוצאה של פעולה שדורשים הרבה משאבים כדי לאתחל או לחשב אותם. לא כדאי לחזור על החישוב הזה בכל פעם שמשנים את הפריסה.
לדוגמה, יצירת האובייקט ShaderBrush היא פעולה יקרה:
val brush = remember { ShaderBrush( BitmapShader( ImageBitmap.imageResource(res, avatarRes).asAndroidBitmap(), Shader.TileMode.REPEAT, Shader.TileMode.REPEAT ) ) }
remember מאחסן את הערך עד שהוא יוצא מה-Composition. עם זאת, יש דרך לבטל את התוקף של הערך שנשמר במטמון. בנוסף, אפשר להעביר ל-API remember פרמטר key או keys. אם אחד מהמפתחות האלה משתנה, בפעם הבאה שהפונקציה remember מרכיבה מחדש את המטמון ומבצעת שוב את בלוק ה-lambda של החישוב. המנגנון הזה מאפשר לכם לשלוט במשך החיים של אובייקט בהרכב. החישוב תקף עד שהערכים משתנים, ולא עד שהערך שנשמר יוצא מהקומפוזיציה.
בדוגמאות הבאות אפשר לראות איך המנגנון הזה פועל.
בקטע הקוד הזה, נוצר ShaderBrush ונעשה בו שימוש כצבע הרקע של קומפוזבילי Box. המשתנה remember מאחסן את מופע ShaderBrush כי יקר ליצור אותו מחדש, כמו שהוסבר קודם. remember מקבל את avatarRes כפרמטר key1, שהוא תמונת הרקע שנבחרה. אם avatarRes משתנה, המברשת מרכיבה מחדש את התמונה החדשה ומחיל אותה מחדש על Box. מצב כזה יכול לקרות כשהמשתמש בוחר תמונה אחרת להיות הרקע מתוך בורר.
@Composable private fun BackgroundBanner( @DrawableRes avatarRes: Int, modifier: Modifier = Modifier, res: Resources = LocalContext.current.resources ) { val brush = remember(key1 = avatarRes) { ShaderBrush( BitmapShader( ImageBitmap.imageResource(res, avatarRes).asAndroidBitmap(), Shader.TileMode.REPEAT, Shader.TileMode.REPEAT ) ) } Box( modifier = modifier.background(brush) ) { /* ... */ } }
בקטע הקוד הבא, המצב מועבר למחזיק מצב פשוט
MyAppState. הוא חושף פונקציה rememberMyAppState כדי לאתחל מופע של המחלקה באמצעות remember. הצגת פונקציות כאלה כדי ליצור מופע ששורד את ההרכבה מחדש היא דפוס נפוץ ב-Compose. הפונקציה
rememberMyAppState מקבלת את windowSizeClass, שמשמש כפרמטר key עבור remember. אם הפרמטר הזה משתנה, האפליקציה צריכה ליצור מחדש את המחלקה של מחזיק המצב הרגיל עם הערך העדכני. זה יכול לקרות אם, לדוגמה, המשתמש מסובב את המכשיר.
@Composable private fun rememberMyAppState( windowSizeClass: WindowSizeClass ): MyAppState { return remember(windowSizeClass) { MyAppState(windowSizeClass) } } @Stable class MyAppState( private val windowSizeClass: WindowSizeClass ) { /* ... */ }
Compose משתמש בהטמעה של equals של המחלקה כדי להחליט אם מפתח השתנה ולבטל את הערך המאוחסן.
אחסון מצב עם מפתחות מעבר לקומפוזיציה מחדש
rememberSaveable API הוא wrapper של remember שיכול לאחסן נתונים ב-Bundle. ממשק ה-API הזה מאפשר לשמור את המצב לא רק אחרי הרכבה מחדש, אלא גם אחרי יצירה מחדש של פעילות וסיום תהליך שהמערכת יזמה.
rememberSaveable מקבל פרמטרים של input לאותה מטרה שבה remember מקבל פרמטרים של keys. המטמון נפסל אם יש שינוי באחד מהקלטות. בפעם הבאה שהפונקציה תורכב מחדש, rememberSaveable יופעל מחדש
בלוק ה-lambda של החישוב.
בדוגמה הבאה, הערך rememberSaveable מאחסן את הערך userTypedQuery עד שהערך typedQuery משתנה:
var userTypedQuery by rememberSaveable(typedQuery, stateSaver = TextFieldValue.Saver) { mutableStateOf( TextFieldValue(text = typedQuery, selection = TextRange(typedQuery.length)) ) }
מידע נוסף
מידע נוסף על מצב ועל Jetpack Compose זמין במקורות המידע הנוספים הבאים.
טעימות
Codelabs
סרטונים
בלוגים
מומלץ
- הערה: טקסט הקישור מוצג כש-JavaScript מושבת
- תכנון הארכיטקטורה של ממשק המשתמש ב-Compose
- שמירת מצב ממשק המשתמש בפיתוח נייטיב
- תופעות לוואי בפיתוח נייטיב