מצב באפליקציה הוא כל ערך שיכול להשתנות לאורך זמן. זוהי הגדרה רחבה מאוד שכוללת הכול, ממסד נתונים מסוג Room ועד למשתנה במחלקה.
כל האפליקציות ל-Android מציגות את המצב למשתמש. דוגמאות למצבים באפליקציות ל-Android:
- סרגל צד שרואים בו מקרים שבהם אי אפשר ליצור חיבור לרשת.
- פוסט בבלוג והתגובות המשויכות אליו.
- אנימציות של הדים בלחצנים שמופעלים כשמשתמש לוחץ עליהם.
- מדבקות שמשתמשים יכולים לצייר מעל תמונה.
בעזרת Jetpack Compose תוכלו לציין בבירור איפה ואיך אתם שומרים את המצב באפליקציית Android ומשתמשים בו. המדריך הזה מתמקד בקשר בין המצב לבין רכיבי ה-Composable, ובממשקי ה-API ש-Jetpack Compose מציע כדי לעבוד עם המצב בקלות רבה יותר.
מצב ויצירה
Compose הוא פונקציונלי, ולכן הדרך היחידה לעדכן אותו היא להפעיל את אותו composable עם ארגומנטים חדשים. הארגומנטים האלה מייצגים את המצב של ממשק המשתמש. בכל פעם שמצב מתעדכן, מתבצעת הרכבה מחדש. כתוצאה מכך, פריטים כמו 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 ושינוי שלהם.
למידע נוסף על הרכבה ראשונית ועל הרכבה מחדש, קראו את המאמר חשיבה ב-Compose.
מצבים בתכנים קומפוזביליים
פונקציות מורכבות יכולות להשתמש ב-API remember
כדי לשמור אובייקט בזיכרון. ערך שמחושב על ידי remember
מאוחסן ב-Composition במהלך ה-composition הראשוני, והערך המאוחסן מוחזר במהלך ה-recomposition.
אפשר להשתמש ב-remember
כדי לאחסן אובייקטים שניתן לשנות ואובייקטים שלא ניתן לשנות.
mutableStateOf
יוצרת משתנה MutableState<T>
שניתן למדידה, שהוא סוג שניתן למדידה שמשולב בסביבת זמן הריצה של compose.
interface MutableState<T> : State<T> {
override var value: T
}
שינויים ב-value
מזמינים יצירת קומפוזיציה מחדש של כל הפונקציות הניתנות ליצירה שמקריאות ל-value
.
יש שלוש דרכים להצהיר על אובייקט MutableState
ב-composable:
val mutableState = remember { mutableStateOf(default) }
var value by remember { mutableStateOf(default) }
val (value, setValue) = remember { mutableStateOf(default) }
ההצהרות האלה זהות, והן ניתנות כתחליף קל לשימוש בתחביר לשימושים שונים במצב. עליכם לבחור את הקוד שמפיק את הקוד הקל ביותר לקריאה בתוכן הקומפוזבילי שאתם כותבים.
כדי להשתמש בתחביר של הנציג by
, נדרשים הייבוא הבאים:
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
. כל ערך שאפשר לשמור ב-Bundle
נשמר באופן אוטומטי באמצעות rememberSaveable
. לערכים אחרים, אפשר להעביר אובייקט שומר מותאם אישית.
סוגי מדינות נתמכים אחרים
ב-Compose אין צורך להשתמש ב-MutableState<T>
כדי לשמור מצב, הוא תומך בסוגי observable אחרים. לפני שקוראים סוג אחר של משתנה שניתן לצפות בו ב-Compose, צריך להמיר אותו ל-State<T>
כדי שרכיבי Compose יוכלו לבצע קומפוזיציה מחדש באופן אוטומטי כשהמצב משתנה.
Compose מגיע עם פונקציות ליצירת State<T>
מטיפים נפוצים של observable שנעשה בהם שימוש באפליקציות ל-Android. לפני שמשתמשים בשילובים האלה, צריך להוסיף את הארטיפקטים המתאימים כפי שמתואר בהמשך:
Flow
:collectAsStateWithLifecycle()
collectAsStateWithLifecycle()
אוסף ערכים מ-Flow
באופן שמתחשב במחזור החיים, וכך מאפשר לאפליקציה לחסוך במשאבי האפליקציה. הוא מייצג את הערך האחרון שהתקבל מסמל הכתיבהState
. מומלץ להשתמש ב-API הזה כדי לאסוף תהליכים באפליקציות ל-Android.התלות הבאה נדרשת בקובץ
build.gradle
(היא צריכה להיות בגרסה 2.6.0-beta01 ואילך):
Kotlin
dependencies {
...
implementation("androidx.lifecycle:lifecycle-runtime-compose:2.8.7")
}
Groovy
dependencies {
...
implementation "androidx.lifecycle:lifecycle-runtime-compose:2.8.7"
}
-
collectAsState
דומה למחרוזתcollectAsStateWithLifecycle
, כי היא אוספת ערכים מ-Flow
וממירה אותו לכתיבהState
.משתמשים ב-
collectAsState
לקוד שלא תלוי בפלטפורמה במקום ב-collectAsStateWithLifecycle
, שמוגבל ל-Android בלבד.אין צורך ביחסי תלות נוספים ל-
collectAsState
כי הוא זמין ב-compose-runtime
. -
observeAsState()
מתחיל לצפות בLiveData
הזה ומייצג את הערכים שלו באמצעותState
.התלות הבאה נדרשת בקובץ
build.gradle
:
Kotlin
dependencies {
...
implementation("androidx.compose.runtime:runtime-livedata:1.7.8")
}
מגניב
dependencies {
...
implementation "androidx.compose.runtime:runtime-livedata:1.7.8"
}
-
subscribeAsState()
הן פונקציות הרחבה שממירות את הזרמים הרספונסיביים של RxJava2 (למשלSingle
, Observable
,Completable
) ל-ComposeState
.בקובץ
build.gradle
נדרשת תלות הבאה:
Kotlin
dependencies {
...
implementation("androidx.compose.runtime:runtime-rxjava2:1.7.8")
}
Groovy
dependencies {
...
implementation "androidx.compose.runtime:runtime-rxjava2:1.7.8"
}
-
subscribeAsState()
הן פונקציות הרחבה שממירות את הזרמים הרספונסיביים של RxJava3 (למשלSingle
, Observable
,Completable
) ל-ComposeState
.התלות הבאה נדרשת בקובץ
build.gradle
:
Kotlin
dependencies {
...
implementation("androidx.compose.runtime:runtime-rxjava3:1.7.8")
}
Groovy
dependencies {
...
implementation "androidx.compose.runtime:runtime-rxjava3:1.7.8"
}
שמירת מצב לעומת ללא שמירת מצב
רכיב מורכב שמשתמש ב-remember
כדי לאחסן אובייקט יוצר מצב פנימי, וכך הרכיב המורכב הופך לרכיב עם מצב. HelloContent
הוא דוגמה לרכיב מורכב עם מצב (stateful) כי הוא שומר ומשנה את המצב שלו (name
) באופן פנימי. האפשרות הזו יכולה להיות שימושית במצבים שבהם מבצע הקריאה לא צריך לשלוט במצב, ויכול להשתמש בו בלי לנהל אותו בעצמו. עם זאת, רכיבים מורכבים עם מצב פנימי נוטים להיות פחות ניתנים לשימוש חוזר וקשה יותר לבדוק אותם.
רכיב ללא מצב הוא רכיב שמורכב מרכיבים אחרים ולא שומר מצב. דרך קלה להשיג מצב ללא שמירת מצב היא באמצעות העלאת המצב.
כשאתם מפתחים רכיבים מורכבים לשימוש חוזר, לעיתים קרובות אתם רוצים לחשוף גם גרסה עם שמירת מצב וגם גרסה ללא שמירת מצב של אותו רכיב מורכב. הגרסה עם שמירת המצב נוחה למתקשרים שלא אכפת להם מהמדינה, והגרסה ללא שמירת מצב נדרשת למתקשרים שצריכים לשלוט במצב או להעלות אותו.
העלאת רמת המדינה
העלאת מצב של פריטים קומפוזביליות במצב נייטיב היא דפוס של העברה למצב קריאה של תוכן קומפוזבילי, כדי להפוך מצב קומפוזבילי ללא שמירת מצב. התבנית הכללית להעברת המצב לחלק העליון של הקוד ב-Jetpack Compose היא להחליף את משתנה המצב בשני פרמטרים:
value: T
: הערך הנוכחי שרוצים להציגonValueChange: (T) -> Unit
: אירוע שמבקש לשנות את הערך, כאשרT
הוא הערך החדש המוצע
עם זאת, אתם לא מוגבלים ל-onValueChange
. אם אירועים ספציפיים יותר מתאימים לתוכן הקומפוזיציה, צריך להגדיר אותם באמצעות פונקציות lambda.
למצבים שמאוחזרים בצורה הזו יש כמה מאפיינים חשובים:
- מקור מרוכז אחד: כשאנחנו מעבירים את המצב במקום להכפיל אותו, אנחנו מוודאים שיש רק מקור מרוכז אחד. כך אפשר למנוע באגים.
- אנקפסולציה: רק רכיבים מורכבים עם מצב יכולים לשנות את המצב שלהם. הוא לגמרי פנימי.
- ניתן לשיתוף: אפשר לשתף מצב שהועבר עם כמה רכיבים מורכבים. אם רציתם לקרוא את
name
בתוכן קומפוזבילי אחר, בעזרת ההרמה תוכלו לעשות את זה. - ניתנים לניתוב: גורמים שמפעילים את הרכיבים הניתנים לקישור ללא מצב יכולים להחליט להתעלם מאירועים או לשנות אותם לפני שינוי המצב.
- מופרדים: המצב של הרכיבים הניתנים לקיפול ללא מצב יכול להישמר בכל מקום. לדוגמה, עכשיו אפשר להעביר את
name
אלViewModel
.
בדוגמה, מחלצים את name
ואת onValueChange
מ-HelloContent
ומעבירים אותם למעלה בעץ, ל-HelloScreen
composable שמפעיל את 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
. האובייקט הופך לניתן לחלוקה, וניתן לקבץ אותו בחבילה. לדוגמה, הקוד הזה יוצר סוג נתונים 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
אפשר לנהל את הרמת המצב הפשוטה בפונקציות הקומפוזביליות עצמן. עם זאת, אם כמות המצבים שצריך לעקוב אחריהם גדלה, או אם צריך לבצע לוגיקה בפונקציות שניתנות ליצירה, מומלץ להעביר את האחריות על הלוגיקה והמצב לכיתות אחרות: מחזקי מצב.
מידע נוסף זמין במסמכי התיעוד בנושא העלאת מצב ב-Compose, או באופן כללי יותר, בדף מאגרי מצב ומצב ממשק המשתמש במדריך הארכיטקטורה.
הפעלה מחדש של חישובי הזיכרון כשהמקשים משתנים
משתמשים לעיתים קרובות ב-API של remember
יחד עם MutableState
:
var name by remember { mutableStateOf("") }
כאן, השימוש בפונקציה remember
מאפשר לערך MutableState
לשרוד תוך כדי יצירת קומפוזיציות מחדש.
באופן כללי, remember
מקבל פרמטר lambda calculation
. כשהפונקציה remember
מופעלת בפעם הראשונה, היא מפעילה את הפונקציה הלוגרית 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
תבטל את התוקף של המטמון ותריץ שוב את בלוק הלוגריתם לחישוב. המנגנון הזה מאפשר לכם לשלוט במחזור החיים של אובייקט ב-Composition. החישוב יישאר תקף עד שהנתונים המזינים ישתנו, במקום עד שהערך שנשמר יוצא מה-Composition.
אפשר לראות בדוגמה הבאה איך המנגנון הזה פועל.
בקטע הקוד הזה, נוצר 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 ) { /* ... */ }
פיתוח נייטיב משתמש בהטמעה של שווה למחלקה כדי לקבוע אם מפתח השתנה ולבטל את התוקף של הערך המאוחסן.
מצב אחסון עם מפתחות מעבר הרכבה מחדש
ה-API rememberSaveable
הוא wrapper במסגרת remember
שיכול לאחסן נתונים ב-Bundle
. ה-API הזה מאפשר למצב לשרוד לא רק לאחר יצירת מחדש, אלא גם לאחר יצירה מחדש של פעילות וסיום תהליך ביוזמת המערכת.
rememberSaveable
מקבל פרמטרים של input
לאותו מטרה שבה remember
מקבל את keys
. המטמון לא תקף כשאחד מהקלטים משתנה. בפעם הבאה שהפונקציה תעבור עיבוד מחדש, rememberSaveable
מריץ מחדש את בלוק הלוגריתם של החישוב.
בדוגמה הבאה, המשתנה rememberSaveable
שומר את הערך של userTypedQuery
עד ש-typedQuery
משתנה:
var userTypedQuery by rememberSaveable(typedQuery, stateSaver = TextFieldValue.Saver) { mutableStateOf( TextFieldValue(text = typedQuery, selection = TextRange(typedQuery.length)) ) }
מידע נוסף
למידע נוסף על סטטוס ו-Jetpack Compose, תוכלו לעיין במקורות המידע הנוספים הבאים.
דוגמיות
Codelabs
סרטונים
בלוגים
מומלץ עבורך
- הערה: טקסט הקישור מוצג כש-JavaScript מושבת
- תכנון הארכיטקטורה של ממשק המשתמש של Compose
- שמירת המצב של ממשק המשתמש בחלונית הכתיבה
- תופעות לוואי ב-Compose