תופעת לוואי היא שינוי במצב האפליקציה שמתרחש מחוץ של פונקציה קומפוזבילית. בגלל תכנים קומפוזביליים מחזור חיים ומאפיינים כמו הרכבים מחדש של תכנים קומפוזביליים, או הרכבים מחדש שאפשר למחוק, תכנים קומפוזביליים עדיף שיהיו השפעות לוואי חינם.
אבל לפעמים נדרשות תופעות לוואי, למשל, כדי להפעיל אירוע, כמו הצגת סרגל אינטראקטיבי או ניווט למסך אחר שצוין מצב. יש לקרוא לפעולות אלה ממערכת מבוקרת מודעת למחזור החיים של התוכן הקומפוזבילי. הנושאים בדף הזה במאמר זה תלמדו על ממשקי ה-API השונים שמשפיעים על ממשקי ה-API ב-Jetpack פיתוח נייטיב.
תרחישים לדוגמה של מצבים והשפעה
כמו שנאמר בקטע Thinking in Compose תיעוד, תכנים קומפוזביליים צריכים להיות ללא תופעות לוואי. מתי צריך ליצור שינויים במצב האפליקציה (כפי שמתואר בניהול מסמכי התיעוד של State>, צריך להשתמש באפקט ממשקי API שמאפשרים לנו לחזות את תופעות הלוואי האלה.
יכולות להיות השפעות שונות על ההצעות לכתיבה, בשימוש יתר. חשוב לוודא שהעבודה שאתם עושים בהם קשורה לממשק המשתמש לא מקוטעת את זרימת הנתונים החד-כיוונית כפי שמוסבר במאמר ניהול מצב תיעוד.
LaunchedEffect
: הרצת פונקציות השעיה בהיקף של תוכן קומפוזבילי
לבצע עבודה על חייו של תוכן קומפוזבילי ולקבל את היכולת לקרוא
בפונקציות השעיה, משתמשים
LaunchedEffect
קומפוזבילי. כאשר LaunchedEffect
נכנס ליצירה ביצירה, הוא משגר
coroutine עם בלוק הקוד שמועבר כפרמטר. הקווטין תהיה
בוטל אם 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
הפעולה הזו תחזור על עצמה בכל משך החיים של התוכן הקומפוזבילי.
rememberCoroutineScope
: קבלת היקף מבוסס-הרכב כדי להשיק קורוטין מחוץ לתוכן קומפוזבילי
LaunchedEffect
היא פונקציה קומפוזבילית, ולכן אפשר להשתמש בה רק בתוך פונקציה קומפוזבילית
פונקציות קומפוזביליות. כדי לשגר קורוטין מחוץ לתוכן קומפוזבילי,
אלא בהיקף כך שהוא יבוטל באופן אוטומטי ברגע שהוא יוצא
הרכב, שימוש
rememberCoroutineScope
אפשר להשתמש גם ב-rememberCoroutineScope
כשצריך לשלוט במחזור החיים של
קואוטין אחד או יותר באופן ידני, לדוגמה, ביטול אנימציה
מתרחש אירוע משתמש.
rememberCoroutineScope
היא פונקציה קומפוזבילית שמחזירה
CoroutineScope
מחויבת לנקודה ביצירה שבה היא נקראת.
ההיקף יבוטל כשהקריאה תעזוב את היצירה.
בהתאם לדוגמה הקודמת, אפשר להשתמש בקוד הזה כדי להציג 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
יופעל מחדש כשאחד מהפרמטרים המרכזיים ישתנה. אבל, בעוד
במצבים מסוימים כדאי לתעד ערך באפקט,
משתנה, אינך רוצה שהאפקט יופעל מחדש. כדי לעשות את זה,
נדרשת להשתמש בפונקציה 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
: אפקטים שצריך לנקות
לתופעות לוואי שצריך לנקות אותן אחרי שינוי המקשים או אם
עלים קומפוזביליים את ההרכב, השתמשו
DisposableEffect
.
אם המקשים DisposableEffect
משתנים, התוכן הקומפוזבילי צריך להיעלם
פעולת הניקוי) של האפקט הנוכחי, ומאפסת על ידי הפעלת האפקט שוב.
לדוגמה, ייתכן שתרצו לשלוח אירועים של ניתוח נתונים על סמך
Lifecycle
אירועים
באמצעות שימוש
LifecycleObserver
כדי להאזין לאירועים האלה ב'כתיבה', צריך להשתמש ב-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
כהצהרה הסופית
בקטע הקוד. אחרת, סביבת הפיתוח המשולבת (IDE) תציג שגיאה בזמן build.
SideEffect
: פרסום מצב הכתיבה בקוד ללא כתיבה
כדי לשתף את מצב הכתיבה עם אובייקטים שלא מנוהלים על ידי הרכבה, משתמשים ברכיב
SideEffect
קומפוזבילי. השימוש ב-SideEffect
מבטיח שההשפעה תתבצע אחרי
יצירה מחדש מוצלחת. לעומת זאת, זה לא נכון
תבצעו השפעה לפני הרכבה מחדש מוצלחת,
כשכותבים את האפקט ישירות בתוכן קומפוזבילי.
לדוגמה, ספריית ניתוח הנתונים עשויה לאפשר לכם לפלח את המשתמשים
אוכלוסייה על ידי צירוף מטא נתונים מותאמים אישית ('מאפייני משתמש' בדוגמה הזו)
לכל האירועים הבאים של ניתוח נתונים. כדי לציין את סוג המשתמש של
המשתמש הנוכחי בספריית ניתוח הנתונים, צריך להשתמש בפרמטר 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
: המרת מצב שאינו 'כתיבה' למצב 'כתיבה'
produceState
מפעילה קורוטין בהיקף של ההרכב שיכול לדחוף ערכים
הוחזרו State
. משתמשים בו כדי
להמיר מצב שאינו 'כתיבה' למצב 'כתיבה', לדוגמה
מבוסס-מינויים כמו Flow
, LiveData
או RxJava
יצירה מוזיקלית.
המפיק יושק כאשר produceState
נכנס ליצירה ביצירה, והוא ישתתף
יבוטל כשהם יוצאים מהיצירה. הערך של 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
: המרה של אובייקט מצב אחד או מספר אובייקטים למצב אחר
בתהליך הכתיבה, מתבצעת הרכבת מחדש בכל פעם שאובייקט מצב שנצפה או קלט קומפוזבילי משתנה. אובייקט מצב או קלט עשויים להשתנות בתדירות גבוהה יותר מכפי שממשק המשתמש צריך לעדכן בפועל, שמובילה להרכבת מחדש שאין בה צורך.
צריך להשתמש בderivedStateOf
פונקציה כשהקלט של תוכן קומפוזבילי משתנים בתדירות גבוהה יותר ממה שאתם צריכים
כדי לכתוב אותו מחדש. לרוב זה קורה כשמשהו משתנה לעיתים קרובות, כמו
מיקום של גלילה, אבל התוכן הקומפוזבילי צריך להגיב אליו רק אחרי שחוצה אותו
סף מסוים. derivedStateOf
יוצר אובייקט חדש במצב 'כתיבה' שלך
יוכלו לראות שהוא יתעדכן רק לפי הצורך. כך היא פועלת
בדומה לתהליכי Kotlin
distinctUntilChanged()
.
שימוש נכון
קטע הקוד הבא מציג תרחיש לדוגמה מתאים ל-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
: המרת מצב הכתיבה ל-flows
שימוש ב-snapshotFlow
כדי להמיר State<T>
לזרימה קרה. snapshotFlow
מפעיל את הבלוק שלו כשהוא נאסף ופולט
התוצאה של State
האובייקטים שנקראו בו. כאשר אחד מהאובייקטים State
קריאה בתוך השינויים בבלוק snapshotFlow
, הזרימה תפלט את הערך החדש
לאוסף שלו אם הערך החדש לא שווה ל-
את הערך הקודם שנפלט (התנהגות זו דומה להתנהגות
Flow.distinctUntilChanged
).
בדוגמה הבאה מוצג אפקט לוואי שמתועד כשהמשתמש גולל. מעבר לפריט הראשון ברשימה לצורך ניתוח נתונים:
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.
אפקטים להפעלה מחדש
חלק מהאפקטים בכתיבה, כמו LaunchedEffect
, produceState
, או
DisposableEffect
, ניקח מספר משתנה של ארגומנטים, מפתחות, שישמשו
לבטל את האפקט הפעיל ולהתחיל אפקט חדש עם המקשים החדשים.
המבנה הטיפוסי של ממשקי ה-API האלה הוא:
EffectName(restartIfThisKeyChanges, orThisKey, orThisKey, ...) { block }
בגלל הדקויות של התנהגות זו, עלולות להתרחש בעיות אם הפרמטרים ששימשו להפעלה מחדש של האפקט, זה לא האפקט הנכון:
- הפעלה מחדש של המכשיר לא תשפיע יותר ממה שהיא אמורה לגרום לבאגים באפליקציה.
- הפעלה מחדש של התוצאות עלולה להיות משמעותית יותר ממה שהיא אמורה להיות לא יעילה.
ככלל אצבע, משתנים ניתנים לשינוי ולא ניתנים לשינוי שמשמשים בבלוק האפקט של
את הקוד צריך להוסיף כפרמטרים לאפקט הקומפוזבילי. מלבד אלה,
אתם יכולים להוסיף עוד פרמטרים כדי לאלץ הפעלה מחדש של האפקט. אם השינוי של
משתנה לא אמור לגרום להפעלה מחדש של האפקט, המשתנה צריך להיות מוקף
ב-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
כי הערך שלהם אף פעם לא משתנה ביצירה עקב שימוש
rememberUpdatedState
אם לא מעבירים את lifecycleOwner
כפרמטר
הוא משתנה, HomeScreen
מתחבר מחדש, אבל DisposableEffect
לא הושמד
מתוך ומופעל מחדש. זה גורם לבעיות כי lifecycleOwner
הלא נכון הוא
שהשתמשנו בה מנקודה זו ואילך.
קבועים כמפתחות
אפשר להשתמש בקבוע כמו true
כמפתח אפקט כדי
לעקוב אחר מחזור החיים של אתר ההתקשרות. יש תרחישים תקינים לשימוש עבור
אותו, כמו בדוגמה LaunchedEffect
שמוצגת למעלה. אבל לפני שנעשה את זה,
כדאי לחשוב פעמיים ולוודא שזה מה שאתם צריכים.
מומלץ עבורך
- הערה: טקסט הקישור מוצג כאשר JavaScript מושבת
- State ו-Jetpack פיתוח נייטיב
- Kotlin ל-Jetpack פיתוח נייטיב
- שימוש בתצוגות מפורטות בהצעות לכתיבה