תופעות לוואי

תופעת לוואי היא שינוי במצב האפליקציה שמתרחש מחוץ של פונקציה קומפוזבילית. בגלל תכנים קומפוזביליים מחזור חיים ומאפיינים כמו הרכבים מחדש של תכנים קומפוזביליים, או הרכבים מחדש שאפשר למחוק, תכנים קומפוזביליים עדיף שיהיו השפעות לוואי חינם.

אבל לפעמים נדרשות תופעות לוואי, למשל, כדי להפעיל אירוע, כמו הצגת סרגל אינטראקטיבי או ניווט למסך אחר שצוין מצב. יש לקרוא לפעולות אלה ממערכת מבוקרת מודעת למחזור החיים של התוכן הקומפוזבילי. הנושאים בדף הזה במאמר זה תלמדו על ממשקי ה-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 שמוצגת למעלה. אבל לפני שנעשה את זה, כדאי לחשוב פעמיים ולוודא שזה מה שאתם צריכים.