תוצאה לוואי היא שינוי במצב האפליקציה שמתרחש מחוץ להיקף של פונקציה שניתנת ליצירה. בגלל מחזור החיים של רכיבים מורכבים ומאפיינים כמו יצירת קומפוזיציות בלתי צפויות, ביצוע קומפוזיציות של רכיבים מורכבים בסדרים שונים או קומפוזיציות שאפשר להשליך, רצוי שהרכיבים המורכבים לא יגרמו לתופעות לוואי.
עם זאת, לפעמים יש צורך בתופעות לוואי, למשל כדי להפעיל אירוע חד-פעמי כמו הצגת סרגל סטטוסים או ניווט למסך אחר, בהתאם לתנאי מצב מסוים. צריך להפעיל את הפעולות האלה בסביבה מבוקרת שמכירה את מחזור החיים של הרכיב הניתן לקישור. בדף הזה נסביר על ממשקי ה-API השונים של תופעות לוואי ש-Jetpack Compose מציע.
תרחישים לדוגמה של מצב ואפקט
כפי שמתואר במסמכי העזרה של Thinking in Compose, רכיבים שניתנים ליצירה צריכים להיות ללא תופעות לוואי. כשצריך לבצע שינויים במצב של האפליקציה (כפי שמתואר במסמך ניהול המצב), צריך להשתמש ב-Effect API כדי שהתוצאות הלוואי יבוצעו באופן צפוי.
בגלל האפשרויות השונות של האפקטים בחלונית הכתיבה, קל להשתמש בהם יותר מדי. חשוב לוודא שהעבודה שאתם מבצעים בהם קשורה לממשק המשתמש, ושלא תגרום לשיבוש זרימת הנתונים החד-כיוונית, כפי שמוסבר במסמכי העזרה בנושא ניהול המצב.
LaunchedEffect
: הפעלת פונקציות השהיה בהיקף של פונקציה הניתנת להגדרה
כדי לבצע משימות במהלך החיים של פונקציה הניתנת להגדרה, ולאפשר קריאה לפונקציות השהיה, צריך להשתמש בפונקציה הניתנת להגדרה LaunchedEffect
. כש-LaunchedEffect
נכנס ל-Composition, הוא מפעיל פונקציית קורוטין עם בלוק הקוד שהועבר כפרמטר. שיתוף הפעולה בין המשימות יבוטל אם LaunchedEffect
יוצא מההרכב. אם LaunchedEffect
יורכב מחדש עם מפתחות שונים (ראו את הקטע הפעלה מחדש של אפקטים בהמשך), קורוטין הקיים יבוטל ופונקציית ההשהיה החדשה תושק בקורוטין חדש.
לדוגמה, הנה אנימציה שבה ערך האלפא פועם עם עיכוב שניתן להגדרה:
// Allow the pulse rate to be configured, so it can be sped up if the user is running // out of time var pulseRateMs by remember { mutableStateOf(3000L) } val alpha = remember { Animatable(1f) } LaunchedEffect(pulseRateMs) { // Restart the effect when the pulse rate changes while (isActive) { delay(pulseRateMs) // Pulse the alpha every pulseRateMs to alert the user alpha.animateTo(0f) alpha.animateTo(1f) } }
בקוד שלמעלה, האנימציה משתמשת בפונקציית ההשהיה delay
כדי להמתין למשך הזמן שהוגדר. לאחר מכן, המערכת יוצרת אנימציה של האלפא ברצף לאפס ובחזרה באמצעות animateTo
.
התהליך הזה יחזור על עצמו כל עוד ה-composable פועל.
rememberCoroutineScope
: קבלת היקף שמודע להרכבה כדי להפעיל פונקציית coroutine מחוץ ל-composable
מכיוון ש-LaunchedEffect
היא פונקציה שניתנת ליצירה, אפשר להשתמש בה רק בתוך פונקציות אחרות שניתנות ליצירה. כדי להפעיל קורוטין מחוץ ל-composable, אבל כך שהוא יבוטל באופן אוטומטי ברגע שהוא יוצא מההרכב, משתמשים ב-rememberCoroutineScope
.
צריך להשתמש ב-rememberCoroutineScope
גם כשצריך לשלוט באופן ידני במחזור החיים של פונקציית coroutine אחת או יותר, למשל, ביטול אנימציה כשמתרחש אירוע של משתמש.
rememberCoroutineScope
היא פונקציה שאפשר ליצור ממנה קומפוזיציות, והיא מחזירה CoroutineScope
שמקושר לנקודה ב-Composition שבה היא נקראת. ההיקף יבוטל כשהשיחה תצא מהעריכה.
בהמשך לדוגמה הקודמת, אפשר להשתמש בקוד הזה כדי להציג Snackbar
כשהמשתמש מקייש על Button
:
@Composable fun MoviesScreen(snackbarHostState: SnackbarHostState) { // Creates a CoroutineScope bound to the MoviesScreen's lifecycle val scope = rememberCoroutineScope() Scaffold( snackbarHost = { SnackbarHost(hostState = snackbarHostState) } ) { contentPadding -> Column(Modifier.padding(contentPadding)) { Button( onClick = { // Create a new coroutine in the event handler to show a snackbar scope.launch { snackbarHostState.showSnackbar("Something happened!") } } ) { Text("Press me") } } } }
rememberUpdatedState
: הפניה לערך באפקט שלא צריך להפעיל מחדש אם הערך משתנה
LaunchedEffect
מופעל מחדש כשאחד מהפרמטרים המרכזיים משתנה. עם זאת, בחלק מהמקרים כדאי לתעד ערך ב-effect, כך שאם הערך ישתנה, לא תתבצע הפעלה מחדש של ה-effect. כדי לעשות זאת, צריך להשתמש ב-rememberUpdatedState
כדי ליצור הפניה לערך הזה, שאפשר לתעד ולעדכן. הגישה הזו שימושית להשפעות שמכילות פעולות לטווח ארוך, שעשויות להיות יקרות או בלתי אפשריות ליצור מחדש ולהפעיל מחדש.
לדוגמה, נניח שבאפליקציה שלכם יש LandingScreen
שמופיע ונעלם אחרי זמן מה. גם אם LandingScreen
יותאם מחדש, האפקט שממתין זמן מה ומציג הודעה על הזמן שחלף לא אמור להתחיל מחדש:
@Composable fun LandingScreen(onTimeout: () -> Unit) { // This will always refer to the latest onTimeout function that // LandingScreen was recomposed with val currentOnTimeout by rememberUpdatedState(onTimeout) // Create an effect that matches the lifecycle of LandingScreen. // If LandingScreen recomposes, the delay shouldn't start again. LaunchedEffect(true) { delay(SplashWaitTimeMillis) currentOnTimeout() } /* Landing screen content */ }
כדי ליצור אפקט שתואם למחזור החיים של אתר הקריאה, מעבירים פרמטר של קבוע שלא משתנה, כמו Unit
או true
. בקוד שלמעלה נעשה שימוש ב-LaunchedEffect(true)
. כדי לוודא שהפונקציה onTimeout
מסוג lambda תמיד מכילה את הערך האחרון שבו LandingScreen
הורכבה מחדש, צריך לעטוף את onTimeout
בפונקציה rememberUpdatedState
.
צריך להשתמש ב-State
, currentOnTimeout
שהוחזר בקוד, בתוצאה.
DisposableEffect
: אפקטים שדורשים ניקוי
כדי לנקות את תופעות הלוואי אחרי ששיניתם את המפתחות, או אם הרכיב הניתן ליצירה יוצא מה-Composition, צריך להשתמש ב-DisposableEffect
.
אם המפתחות של DisposableEffect
משתנים, הרכיב הניתן לקיבוץ צריך להיפטר (לנקות) מהאפקט הנוכחי שלו, ולאפס אותו על ידי קריאה חוזרת לאפקט.
לדוגמה, יכול להיות שתרצו לשלוח אירועי ניתוח נתונים על סמך אירועי Lifecycle
באמצעות LifecycleObserver
.
כדי להאזין לאירועים האלה ב-Compose, משתמשים ב-DisposableEffect
כדי לרשום ולבטל את הרישום של הצופה לפי הצורך.
@Composable fun HomeScreen( lifecycleOwner: LifecycleOwner = LocalLifecycleOwner.current, onStart: () -> Unit, // Send the 'started' analytics event onStop: () -> Unit // Send the 'stopped' analytics event ) { // Safely update the current lambdas when a new one is provided val currentOnStart by rememberUpdatedState(onStart) val currentOnStop by rememberUpdatedState(onStop) // If `lifecycleOwner` changes, dispose and reset the effect DisposableEffect(lifecycleOwner) { // Create an observer that triggers our remembered callbacks // for sending analytics events val observer = LifecycleEventObserver { _, event -> if (event == Lifecycle.Event.ON_START) { currentOnStart() } else if (event == Lifecycle.Event.ON_STOP) { currentOnStop() } } // Add the observer to the lifecycle lifecycleOwner.lifecycle.addObserver(observer) // When the effect leaves the Composition, remove the observer onDispose { lifecycleOwner.lifecycle.removeObserver(observer) } } /* Home screen content */ }
בקוד שלמעלה, האפקט יוסיף את הערך observer
לערך lifecycleOwner
. אם הערך של lifecycleOwner
ישתנה, האפקט יוסר ויופעל מחדש עם הערך החדש של lifecycleOwner
.
DisposableEffect
חייב לכלול תנאי onDispose
כטענת הקצה בבלוק הקוד שלו. אחרת, תופיע שגיאה בזמן ה-build בסביבת הפיתוח המשולבת.
SideEffect
: פרסום המצב של Compose בקוד שאינו של Compose
כדי לשתף את המצב של Compose עם אובייקטים שלא מנוהלים על ידי Compose, משתמשים ב-composable SideEffect
. שימוש ב-SideEffect
מבטיח שהאפקט יבוצע אחרי כל יצירת קומפוזיציה מחדש מוצלחת. מצד שני, לא נכון לבצע אפקט לפני שאפשר להבטיח שהרכבה מחדש תתבצע בהצלחה. זה המצב כשכותבים את האפקט ישירות ב-composable.
לדוגמה, ייתכן שספריית הניתוח תאפשר לכם לפלח את אוכלוסיית המשתמשים על ידי צירוף מטא-נתונים מותאמים אישית ('מאפייני משתמש' בדוגמה הזו) לכל האירועים הבאים בניתוח. כדי לעדכן את הערך של SideEffect
ולשלוח את סוג המשתמש של המשתמש הנוכחי לספריית Analytics, משתמשים ב-SideEffect
.
@Composable fun rememberFirebaseAnalytics(user: User): FirebaseAnalytics { val analytics: FirebaseAnalytics = remember { FirebaseAnalytics() } // On every successful composition, update FirebaseAnalytics with // the userType from the current User, ensuring that future analytics // events have this metadata attached SideEffect { analytics.setUserProperty("userType", user.userType) } return analytics }
produceState
: המרת מצב שאינו Compose למצב Compose
produceState
מפעילה קורוטין ברמת הקומפוזיציה, שיכול לדחוף ערכים ל-State
המוחזר. אפשר להשתמש בו כדי להמיר מצב שאינו של Compose למצב של Compose. לדוגמה, אפשר להוסיף ל-Composition מצב חיצוני שמבוסס על מינויים, כמו Flow
, LiveData
או RxJava
.
ה-producer יופעל כש-produceState
ייכנס ל-Composition, ויבוטל כשהוא ייצא ממנו. הערך המוחזר של State
משולב. הגדרת אותו ערך לא תגרום ליצירה מחדש.
למרות ש-produceState
יוצרת קורוטין, אפשר להשתמש בה גם כדי לצפות במקורות נתונים שלא מושהים. כדי להסיר את המינוי למקור הזה, משתמשים בפונקציה awaitDispose
.
בדוגמה הבאה מוסבר איך משתמשים ב-produceState
כדי לטעון תמונה מהרשת. הפונקציה הניתנת לקישור loadNetworkImage
מחזירה State
שאפשר להשתמש בו ברכיבים אחרים שניתן לקשר.
@Composable fun loadNetworkImage( url: String, imageRepository: ImageRepository = ImageRepository() ): State<Result<Image>> { // Creates a State<T> with Result.Loading as initial value // If either `url` or `imageRepository` changes, the running producer // will cancel and will be re-launched with the new inputs. return produceState<Result<Image>>(initialValue = Result.Loading, url, imageRepository) { // In a coroutine, can make suspend calls val image = imageRepository.load(url) // Update State with either an Error or Success result. // This will trigger a recomposition where this State is read value = if (image == null) { Result.Error } else { Result.Success(image) } } }
derivedStateOf
: המרת אובייקט מצב אחד או יותר למצב אחר
ב-Compose, הרכבה מחדש מתרחשת בכל פעם שמשתנה אובייקט מצב שנצפה או קלט שאפשר להרכיב. אובייקט מצב או קלט עשויים להשתנות בתדירות גבוהה יותר מהתדירות שבה ממשק המשתמש צריך להתעדכן, וכתוצאה מכך מתבצעת יצירת מחדש מיותרת.
כדאי להשתמש בפונקציה derivedStateOf
כשהקלטים של הרכיב הניתן לקיבוץ משתנים בתדירות גבוהה יותר מהצורך לבצע יצירת מחדש. המצב הזה מתרחש לעיתים קרובות כשמשהו משתנה בתדירות גבוהה, כמו מיקום גלילה, אבל הרכיב המודולרי צריך להגיב לכך רק אחרי שהוא חוצה ערך סף מסוים. derivedStateOf
יוצר אובייקט חדש של מצב Compose, שאפשר לראות שהוא מתעדכן רק לפי הצורך. כך הוא פועל באופן דומה לאופרטור distinctUntilChanged()
ב-Kotlin Flows.
שימוש נכון
קטע הקוד הבא מציג תרחיש לדוגמה לשימוש ב-derivedStateOf
:
@Composable // When the messages parameter changes, the MessageList // composable recomposes. derivedStateOf does not // affect this recomposition. fun MessageList(messages: List<Message>) { Box { val listState = rememberLazyListState() LazyColumn(state = listState) { // ... } // Show the button if the first visible item is past // the first item. We use a remembered derived state to // minimize unnecessary compositions val showButton by remember { derivedStateOf { listState.firstVisibleItemIndex > 0 } } AnimatedVisibility(visible = showButton) { ScrollToTopButton() } } }
בקטע הקוד הזה, הערך של firstVisibleItemIndex
משתנה בכל פעם שהפריט הראשון שגלוי משתנה. ככל שמגללים, הערך הופך ל-0
, 1
, 2
, 3
, 4
, 5
וכו'. עם זאת, המערכת צריכה לבצע יצירת קומפוזיציה מחדש רק אם הערך גדול מ-0
.
חוסר ההתאמה בתדירות העדכונים מצביע על כך שזהו תרחיש לדוגמה מתאים ל-derivedStateOf
.
שימוש שגוי
טעות נפוצה היא להניח שכשמאחדים שני אובייקטים של מצב ב-Compose, צריך להשתמש ב-derivedStateOf
כי מדובר ב'יצירת מצב'. עם זאת, זהו יתרון נוסף בלבד, ולא חובה להשתמש בו, כפי שמוצג בקטע הקוד הבא:
// DO NOT USE. Incorrect usage of derivedStateOf. var firstName by remember { mutableStateOf("") } var lastName by remember { mutableStateOf("") } val fullNameBad by remember { derivedStateOf { "$firstName $lastName" } } // This is bad!!! val fullNameCorrect = "$firstName $lastName" // This is correct
בקטע הקוד הזה, fullName
צריך להתעדכן בתדירות זהה לזו של firstName
ו-lastName
. לכן, לא מתבצעת יצירת קומפוזיציה מחדש מיותרת ואין צורך להשתמש ב-derivedStateOf
.
snapshotFlow
: המרת המצב של Compose ל-Flows
משתמשים ב-snapshotFlow
כדי להמיר אובייקטים מסוג State<T>
ל-Flow קר. snapshotFlow
מפעיל את הבלוק שלו כשהוא נאסף ומפיק את התוצאה של אובייקטי State
שנקראים בו. כשאחד מהאובייקטים של State
שנקראו בתוך הבלוק snapshotFlow
עובר טרנספורמציה, ה-Flow יפיק את הערך החדש לאוסף שלו אם הערך החדש לא שווה לערך הקודם שהופיק (ההתנהגות הזו דומה לזו של Flow.distinctUntilChanged
).
בדוגמה הבאה מוצגת תופעת לוואי שמתעדת ב-Analytics את האירוע שבו המשתמש גולל מעבר לפריט הראשון ברשימה:
val listState = rememberLazyListState()
LazyColumn(state = listState) {
// ...
}
LaunchedEffect(listState) {
snapshotFlow { listState.firstVisibleItemIndex }
.map { index -> index > 0 }
.distinctUntilChanged()
.filter { it == true }
.collect {
MyAnalyticsService.sendScrolledPastFirstItemEvent()
}
}
בקוד שלמעלה, הערך listState.firstVisibleItemIndex
מומר ל-Flow שאפשר להשתמש בו עם אופרטורים של Flow.
הפעלה מחדש של אפקטים
אפקטים מסוימים ב-Compose, כמו LaunchedEffect
, produceState
או DisposableEffect
, מקבלים מספר משתנה של ארגומנטים (מפתחות) שמשמשים לבטלת האפקט שפועל ולהתחיל אפקט חדש עם המפתחות החדשים.
הפורמט האופייני לממשקי ה-API האלה הוא:
EffectName(restartIfThisKeyChanges, orThisKey, orThisKey, ...) { block }
בגלל המורכבות של ההתנהגות הזו, יכולות להתרחש בעיות אם הפרמטרים המשמשים להפעלה מחדש של האפקט לא נכונים:
- הפעלה מחדש של אפקטים בתדירות נמוכה מהנדרש עלולה לגרום לבאגים באפליקציה.
- הפעלה מחדש של אפקטים יותר מהנדרש עלולה להיות לא יעילה.
ככלל אצבע, צריך להוסיף משתנים שניתן לשנות ומשתנים שלא ניתן לשנות, שמשמשים בבלוק הקוד של האפקט, כפרמטרים ל-effect composable. בנוסף, אפשר להוסיף פרמטרים נוספים כדי לאלץ הפעלה מחדש של האפקט. אם השינוי במשתנה לא אמור לגרום להפעלה מחדש של האפקט, צריך לעטוף את המשתנה ב-rememberUpdatedState
. אם המשתנה לא משתנה אף פעם כי הוא עטוף ב-remember
ללא מפתחות, אין צורך להעביר את המשתנה כמפתח לאפקט.
בקוד DisposableEffect
שמוצג למעלה, האפקט מקבל כפרמטר את הערך של lifecycleOwner
שמשמש בבלוק שלו, כי כל שינוי בהם אמור לגרום להפעלה מחדש של האפקט.
@Composable
fun HomeScreen(
lifecycleOwner: LifecycleOwner = LocalLifecycleOwner.current,
onStart: () -> Unit, // Send the 'started' analytics event
onStop: () -> Unit // Send the 'stopped' analytics event
) {
// These values never change in Composition
val currentOnStart by rememberUpdatedState(onStart)
val currentOnStop by rememberUpdatedState(onStop)
DisposableEffect(lifecycleOwner) {
val observer = LifecycleEventObserver { _, event ->
/* ... */
}
lifecycleOwner.lifecycle.addObserver(observer)
onDispose {
lifecycleOwner.lifecycle.removeObserver(observer)
}
}
}
אין צורך ב-currentOnStart
וב-currentOnStop
כמפתחות DisposableEffect
, כי הערך שלהם אף פעם לא משתנה ב-Composition בגלל השימוש ב-rememberUpdatedState
. אם לא מעבירים את lifecycleOwner
כפרמטר והוא משתנה, HomeScreen
יעבור קומפוזיציה מחדש, אבל ה-DisposableEffect
לא ינוקה ויופעל מחדש. זה גורם לבעיות כי מאותו רגע ואילך נעשה שימוש ב-lifecycleOwner
הלא נכון.
קבועים כמפתחות
אפשר להשתמש בערך קבוע כמו true
כמפתח אפקט כדי שהוא יעקוב אחרי מחזור החיים של אתר הקריאה. יש תרחישים לדוגמה שבהם אפשר להשתמש באפשרות הזו, כמו הדוגמה של LaunchedEffect
שמוצגת למעלה. עם זאת, לפני שתעברו לכך, כדאי לחשוב פעמיים ולוודא שזה מה שאתם צריכים.
מומלץ עבורך
- הערה: טקסט הקישור מוצג כש-JavaScript מושבת
- State ו-Jetpack פיתוח נייטיב
- Kotlin ל-Jetpack פיתוח נייטיב
- שימוש בתצוגות ב'כתיבה'