תופעת לוואי היא שינוי במצב של האפליקציה שמתרחש מחוץ להיקף של פונקציה קומפוזבילית. בגלל מחזור החיים של רכיבים קומפוזביליים ומאפיינים כמו הרכבות מחדש בלתי צפויות, הרכבות מחדש של רכיבים קומפוזביליים בסדרים שונים או הרכבות מחדש שאפשר לבטל, רצוי שרכיבים קומפוזביליים יהיו ללא השפעות משניות.
עם זאת, לפעמים יש צורך בפעולות משניות, למשל כדי להפעיל אירוע חד-פעמי כמו הצגת סרגל התראות או ניווט למסך אחר בהתאם לתנאי מצב מסוים. צריך לקרוא לפעולות האלה מסביבה מבוקרת שמודעת למחזור החיים של ה-composable. בדף הזה נסביר על ממשקי ה-API השונים של תופעות הלוואי ש-Jetpack פיתוח נייטיב מציע.
תרחישי שימוש של מצב ואפקט
כמו שמוסבר במאמרי העזרה בנושא חשיבה בפיתוח נייטיב, רכיבים קומפוזביליים צריכים להיות נטולי תופעות לוואי. כשצריך לבצע שינויים במצב של האפליקציה (כמו שמתואר במאמרי העזרה לניהול מצבים), צריך להשתמש בממשקי ה-API של Effect כדי שתופעות הלוואי האלה יבוצעו באופן צפוי.
בגלל האפשרויות השונות שהאפקטים פותחים ב-Compose, קל להשתמש בהם יותר מדי. חשוב לוודא שהעבודה שאתם מבצעים בהם קשורה לממשק המשתמש ולא משבשת את זרימת הנתונים החד-כיוונית, כפי שמוסבר במסמכי התיעוד בנושא ניהול המצב.
LaunchedEffect: הרצת פונקציות השהיה בהיקף של פונקציה קומפוזבילית
כדי לבצע עבודה במהלך מחזור החיים של רכיב קומפוזבילי ולקבל את היכולת להפעיל פונקציות השעיה, משתמשים ברכיב הקומפוזבילי LaunchedEffect. כש-LaunchedEffect נכנס לקומפוזיציה, הוא מפעיל פונקציית Coroutine עם בלוק הקוד שהועבר כפרמטר. ה-coroutine יבוטל אם LaunchedEffect ייצא מהקומפוזיציה. אם LaunchedEffect מורכב מחדש עם מפתחות שונים (ראו את הקטע הפעלה מחדש של אפקטים בהמשך), ה-coroutine הקיים יבוטל ופונקציית ההשהיה החדשה תופעל ב-coroutine חדש.
לדוגמה, הנה אנימציה שמשנה את ערך האלפא עם השהיה שאפשר להגדיר:
// 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 { mutableLongStateOf(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: קבלת היקף שמותאם לקומפוזיציה כדי להפעיל פונקציית coroutine מחוץ לפונקציה הניתנת ליצירה
LaunchedEffect היא פונקציה קומפוזבילית, ולכן אפשר להשתמש בה רק בתוך פונקציות קומפוזביליות אחרות. כדי להפעיל פונקציית coroutine מחוץ לפונקציה קומפוזבילית, אבל עם היקף שמאפשר לבטל אותה באופן אוטומטי כשהיא יוצאת מהקומפוזיציה, משתמשים ב-rememberCoroutineScope.
כדאי להשתמש ב-rememberCoroutineScope גם כשצריך לשלוט באופן ידני במחזור החיים של קורוטינה אחת או יותר, למשל ביטול של אנימציה כשמתרחש אירוע משתמש.
rememberCoroutineScope היא פונקציה קומפוזבילית שמחזירה CoroutineScope שקשור לנקודה בקומפוזיציה שבה היא נקראת. ה-scope יבוטל כשהקריאה תצא מהקומפוזיציה.
בהמשך לדוגמה הקודמת, אפשר להשתמש בקוד הזה כדי להציג 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: אפקטים שצריך לנקות
כדי לנקות את תופעות הלוואי אחרי שהמפתחות משתנים או אם הפונקציה ה-composable יוצאת מהקומפוזיציה, משתמשים ב-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 כהצהרה האחרונה בבלוק הקוד שלו. אחרת, ב-IDE תוצג שגיאה בזמן ה-build.
SideEffect: פרסום מצב Compose בקוד שאינו Compose
כדי לשתף ערך דינמי של Compose עם אובייקטים שלא מנוהלים על ידי Compose, משתמשים בפונקציה הקומפוזבילית 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: המרת מצב שאינו של Compose למצב של Compose
produceState
מפעיל coroutine שמוגדרת לו קומפוזיציה שיכולה לדחוף ערכים אל State שהוחזר. משתמשים בו כדי להמיר מצב שאינו של Compose למצב של Compose, למשל כדי להביא לקומפוזיציה מצב חיצוני שמבוסס על מינוי, כמו Flow, LiveData או RxJava.
ה-producer מופעל כש-produceState נכנס לקומפוזיציה, ומבוטל כשהוא יוצא מהקומפוזיציה. הערך המוחזר של State משתלב.
הגדרת אותו ערך לא תפעיל קומפוזיציה מחדש.
למרות ש-produceState יוצר coroutine, אפשר להשתמש בו גם כדי לצפות במקורות נתונים שלא מושהים. כדי להסיר את המינוי למקור הזה, משתמשים בפונקציה 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 יוצרת אובייקט מצב חדש של Compose שאפשר לצפות בו, והוא מתעדכן רק לפי הצורך. במובן הזה, הוא פועל באופן דומה לאופרטור distinctUntilChanged() של Kotlin Flow.
שימוש נכון
בקטע הקוד הבא מוצג תרחיש לדוגמה שמתאים לשימוש ב-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: המרת State של Compose ל-Flows
משתמשים ב-snapshotFlow כדי להמיר אובייקטים מסוג State<T> ל-Flow קר. snapshotFlow מפעיל את הבלוק שלו כשהוא נאסף ופולט את התוצאה של האובייקטים מסוג State שנקראו בו. כשמתבצעת מוטציה באחד מאובייקטי State שנקראים בתוך הבלוק snapshotFlow, ה-Flow יפלוט את הערך החדש לאספן שלו אם הערך החדש לא שווה לערך הקודם שנפלט (ההתנהגות הזו דומה לזו של 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 שיכול להפיק תועלת מהיכולות של האופרטורים של Flow.
האפקטים מופעלים מחדש
חלק מהאפקטים ב-Compose, כמו 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 מושבת
- מצב ו-Jetpack פיתוח נייטיב
- Kotlin ל-Jetpack Compose
- שימוש ב-Views בפיתוח נייטיב