מצב באפליקציה הוא כל ערך שיכול להשתנות לאורך זמן. זו הגדרה רחבה מאוד שכוללת כל דבר, החל ממסד נתונים של Room ועד למשתנה בכיתה.
כל אפליקציות Android מציגות את הסטטוס למשתמש. דוגמאות למצב באפליקציות ל-Android:
- סרגל אינטראקטיבי שמוצג כשאי אפשר ליצור חיבור לרשת.
- פוסט בבלוג והתגובות שקשורות אליו.
- אנימציות של גלים בלחצנים שמופעלות כשמשתמש לוחץ עליהם.
- מדבקות שמשתמש יכול לצייר מעל תמונה.
Jetpack פיתוח נייטיב עוזר לכם להגדיר במפורש איפה ואיך אתם מאחסנים ומשתמשים במצב באפליקציית Android. המדריך הזה מתמקד בקשר בין מצב לרכיבים הניתנים להרכבה, ובממשקי ה-API ש-Jetpack פיתוח נייטיב מציע כדי לעבוד עם מצב בצורה קלה יותר.
מצב והרכב
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> כדי לשמור את המצב, כי הוא תומך בסוגים אחרים של נתונים שניתן לצפות בהם. לפני שקוראים סוג אחר של observable ב-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.10.0")
}
מגניב
dependencies {
...
implementation "androidx.lifecycle:lifecycle-runtime-compose:2.10.0"
}
-
הפונקציה
collectAsStateדומה לפונקציהcollectAsStateWithLifecycle, כי היא גם אוספת ערכים מ-Flowומשנה אותם ל-ComposeState.כדי להשתמש בקוד שמתאים לכל הפלטפורמות, צריך להשתמש ב-
collectAsStateבמקום ב-collectAsStateWithLifecycle, שמתאים רק ל-Android.לא נדרשים יחסי תלות נוספים בשביל
collectAsState, כי הוא זמין ב-compose-runtime. -
observeAsState()מתחיל לעקוב אחריLiveDataומציג את הערכים שלו באמצעותState.התלות הבאה נדרשת בקובץ
build.gradle:
Kotlin
dependencies {
...
implementation("androidx.compose.runtime:runtime-livedata:1.11.0")
}
מגניב
dependencies {
...
implementation "androidx.compose.runtime:runtime-livedata:1.11.0"
}
-
subscribeAsState()הן פונקציות הרחבה שממירות את הזרמים הריאקטיביים של RxJava2 (למשלSingle, Observable,Completable) ל-Stateשל Compose.התלות הבאה נדרשת בקובץ
build.gradle:
Kotlin
dependencies {
...
implementation("androidx.compose.runtime:runtime-rxjava2:1.11.0")
}
מגניב
dependencies {
...
implementation "androidx.compose.runtime:runtime-rxjava2:1.11.0"
}
-
subscribeAsState()הן פונקציות הרחבה שממירות את הזרמים הריאקטיביים של RxJava3 (לדוגמה,Single,Observable,Completable) ל-Stateשל Compose.התלות הבאה נדרשת בקובץ
build.gradle:
Kotlin
dependencies {
...
implementation("androidx.compose.runtime:runtime-rxjava3:1.11.0")
}
מגניב
dependencies {
...
implementation "androidx.compose.runtime:runtime-rxjava3:1.11.0"
}
עם שמירת מצב לעומת בלי שמירת מצב
קומפוזבל שמשתמש ב-remember כדי לאחסן אובייקט יוצר מצב פנימי, ולכן הקומפוזבל הוא עם שמירת מצב. HelloContent היא דוגמה לרכיב שאפשר להוסיף לו מצב כי הוא מחזיק את המצב name ומשנה אותו באופן פנימי. האפשרות הזו יכולה להיות שימושית במצבים שבהם המתקשר לא צריך לשלוט במצב ויכול להשתמש בו בלי לנהל את המצב בעצמו. עם זאת, קומפוזיציות עם מצב פנימי נוטות להיות פחות ניתנות לשימוש חוזר וקשה יותר לבדוק אותן.
קומפוזבילי בלי שמירת מצב הוא קומפוזבילי שלא מחזיק מצב. דרך קלה להשיג מצב בלי שמירת מצב היא באמצעות העלאת הרמה של מצב (state hoisting).
כשמפתחים פונקציות Composable שאפשר לעשות בהן שימוש חוזר, לרוב רוצים לחשוף גם גרסה עם שמירת מצב וגם גרסה בלי שמירת מצב של אותה פונקציית Composable. הגרסה עם שמירת מצב נוחה למתקשרים שלא אכפת להם מהמצב, והגרסה ללא שמירת מצב נחוצה למתקשרים שצריכים לשלוט במצב או להעביר אותו.
העלאת הרמה של מצב (state hoisting)
העלאת מצב (State hoisting) ב-Compose היא דפוס של העברת מצב לקומפוזבילי של המתקשר כדי להפוך את הקומפוזבילי לחסר מצב (stateless). הדפוס הכללי להעלאת הרמה של מצב (state hoisting) ב-Jetpack פיתוח נייטיב הוא להחליף את משתנה המצב בשני פרמטרים:
-
value: T: הערך הנוכחי שיוצג -
onValueChange: (T) -> Unit: אירוע שמבקש לשנות את הערך, כאשרTהוא הערך החדש המוצע
עם זאת, אתם לא מוגבלים ל-onValueChange. אם יש אירועים ספציפיים יותר שמתאימים לקומפוזבל, צריך להגדיר אותם באמצעות ביטויי למבדה.
למצב שמועבר בדרך הזו יש כמה מאפיינים חשובים:
- מקור אמת יחיד: העברת מצב במקום שכפול שלו מבטיחה שיש רק מקור אמת יחיד. כך אפשר להימנע מבאגים.
- מוכלים: רק קומפוזיציות עם מצב יכולות לשנות את המצב שלהן. הוא פנימי לחלוטין.
- ניתן לשיתוף: אפשר לשתף מצב שהועבר עם כמה רכיבים קומפוזביליים. אם רוצים לקרוא את
nameבקומפוזיציה אחרת, אפשר לעשות את זה באמצעות העברה. - ניתן ליירוט: המתקשרים לפונקציות הניתנות להרכבה ללא מצב יכולים להחליט להתעלם מאירועים או לשנות אותם לפני שינוי המצב.
- מנותק: המצב של רכיבי ה-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 שמציגים מצב בממשק המשתמש לבין החלקים באפליקציה שמאחסנים ומשנים את המצב.
מידע נוסף זמין בדף איפה כדאי להעביר את הסטייט.
שחזור מצב ב-Compose
התנהגות ה-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) פשוטה בפונקציות הניתנות להרכבה עצמן. עם זאת, אם כמות הנתונים שצריך לעקוב אחריהם גדלה, או אם יש צורך בלוגיקה לביצוע בפונקציות שניתנות להרכבה, מומלץ להעביר את האחריות על הלוגיקה והנתונים למחלקות אחרות: מחזיקי מצב.
מידע נוסף זמין במאמרי העזרה בנושא state hoisting ב-Compose או בדף State holders and UI State במדריך הארכיטקטורה.
הפעלה מחדש של חישובי הזיכרון כשמשנים מקשים
remember API משמש לעיתים קרובות יחד עם MutableState:
var name by remember { mutableStateOf("") }
במקרה הזה, השימוש בפונקציה remember מאפשר לערך MutableState לשרוד את ההרכבות מחדש.
באופן כללי, הפונקציה remember מקבלת פרמטר lambda 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. הצגה של פונקציות כאלה כדי ליצור מופע שממשיך להתקיים את ההרכבות מחדש היא דפוס נפוץ בפיתוח נייטיב. הפונקציה
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 פיתוח נייטיב זמין במקורות המידע הבאים.
דוגמאות
Codelabs
סרטונים
בלוגים
מומלץ בשבילכם
- הערה: טקסט הקישור מוצג כש-JavaScript מושבת
- תכנון הארכיטקטורה של ממשק המשתמש ב-Compose
- שמירת מצב ממשק המשתמש בפיתוח נייטיב
- תופעות לוואי בפיתוח נייטיב