Nebeneffekte in „Compose“

Ein Nebeneffekt ist eine Statusänderung der App, die außerhalb des Umfang einer zusammensetzbaren Funktion. Aufgrund von zusammensetzbaren Funktionen Lebenszyklus und Eigenschaften wie unvorhersehbare Neuzusammensetzungen, die Ausführung von Zusammensetzungen von Zusammensetzungen in unterschiedlicher Reihenfolge oder Neuzusammensetzungen, die verworfen werden können, sollten zusammensetzbare Funktionen idealerweise Nebenwirkungen haben kostenlos.

Manchmal sind jedoch Nebenwirkungen erforderlich, z. B. um ein einmaliges Ereignis auszulösen. etwa das Einblenden einer Snackbar oder das Navigieren zu einem anderen Bildschirm, Zustand. Diese Aktionen sollten von einem kontrollierten der den Lebenszyklus der zusammensetzbaren Funktion berücksichtigt. Auf dieser Seite erfahren Sie mehr über die verschiedenen Nebeneffekt-APIs, die Jetpack Compose bietet.

Zustand und Wirkung – Anwendungsfälle

Weitere Informationen hierzu finden Sie im Artikel Thinking in Compose sollten zusammensetzbare Funktionen keine Nebenwirkungen haben. Wenn Sie Änderungen am App-Status vorgenommen werden, wie in den Abschnitten Verwalten Dokument zur Dokumentation des Bundesstaates, sollten Sie den Effect APIs so, dass diese Nebenwirkungen vorhersehbar ausgeführt werden.

Da sich in „Compose“ viele verschiedene Effekte ergeben, zu häufig verwendet wird. Stellen Sie sicher, dass Ihre Arbeit unterbricht den unidirektionalen Datenfluss nicht, wie unter Verwaltungsstatus Dokumentation.

LaunchedEffect: Beendigungsfunktionen im Bereich einer zusammensetzbaren Funktion ausführen

Um während der gesamten Lebensdauer einer zusammensetzbaren Funktion zu arbeiten und die Möglichkeit zu haben, aussetzen, verwenden Sie LaunchedEffect zusammensetzbar. Wenn LaunchedEffect in die Komposition eintritt, wird ein coroutine mit dem als Parameter übergebenen Codeblock. Die Koroutine ist wird abgebrochen, wenn LaunchedEffect die Komposition verlässt. Wenn LaunchedEffect gleich mit unterschiedlichen Tasten angeordnet (siehe Abschnitt Neustart Effekte unten), wird die bestehende Koroutine abgebrochen und die neue Unterbrechungsfunktion wird in einer neuen Koroutine gestartet.

Hier sehen Sie als Beispiel eine Animation, bei der der Alphawert mit einem Konfigurierbare Verzögerung:

// 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)
    }
}

Im obigen Code verwendet die Animation die Unterbrechungsfunktion. delay die festgelegte Zeit zu warten. Dann wird die Alpha-Version der Reihe nach auf null und wieder zurück, animateTo Dies wiederholt sich für die gesamte Lebensdauer der zusammensetzbaren Funktion.

rememberCoroutineScope: Erlangt einen kompositionsbezogenen Bereich, um eine Koroutine außerhalb einer zusammensetzbaren Funktion zu starten.

Da LaunchedEffect eine zusammensetzbare Funktion ist, kann sie nur in anderen zusammensetzbaren Funktionen verwenden. Um eine Koroutine außerhalb einer zusammensetzbaren Funktion zu starten, aber auf einen Gültigkeitsbereich begrenzt, sodass er automatisch abgebrochen wird, sobald er den Bereich Zusammensetzung, Verwendung rememberCoroutineScope Verwenden Sie rememberCoroutineScope auch, wenn Sie den Lebenszyklus eines eine oder mehrere Koroutinen manuell erstellen, z. B. das Abbrechen einer Animation, wenn eine Nutzerereignis eintritt.

rememberCoroutineScope ist eine zusammensetzbare Funktion, die einen Wert CoroutineScope an den Punkt der Komposition gebunden, an der sie aufgerufen wird. Die wird abgebrochen, wenn der Aufruf die Komposition verlässt.

Entsprechend dem vorherigen Beispiel könnten Sie mit diesem Code eine Snackbar anzeigen. wenn der Nutzer auf Button tippt:

@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: Verweist auf einen Wert in einem Effekt, der nicht neu gestartet werden sollte, wenn sich der Wert ändert

LaunchedEffect wird neu gestartet, wenn sich einer der Schlüsselparameter ändert. In In einigen Situationen können Sie einen Wert für Ihren Effekt erfassen, der, möchten, dass der Effekt nicht erneut auftritt. Dazu muss rememberUpdatedState verwenden, um einen Verweis auf diesen Wert zu erstellen, erfasst und aktualisiert werden kann. Dieser Ansatz ist hilfreich für Effekte, die langlebigen Abläufen, deren Neuerstellung kostspielig oder zu kostspielig sein kann, neu starten.

Angenommen, in Ihrer App ist ein LandingScreen vorhanden, das nach einigen Sekunden verschwindet. . Auch bei einer Neuzusammensetzung von LandingScreen ist der Effekt, der eine Weile wartet, und benachrichtigt Sie, dass die abgelaufene Zeit nicht neu gestartet werden sollte:

@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 */
}

Um einen Effekt zu erzeugen, der dem Lebenszyklus der Anrufwebsite entspricht, Eine nie ändernde Konstante wie Unit oder true wird als Parameter übergeben. Im Code oben enthält, wird LaunchedEffect(true) verwendet. Damit die onTimeout Lambda enthält immer den letzten Wert, der von LandingScreen neu zusammengesetzt wurde Dabei muss onTimeout mit der Funktion rememberUpdatedState zusammengefasst werden. Die zurückgegebene State, currentOnTimeout im Code, sollte im folgenden Beispiel verwendet werden: Effekts.

DisposableEffect: Effekte, die bereinigt werden müssen

Für Nebeneffekte, die nach dem Ändern der Tasten bereinigt werden müssen, oder wenn das verlässt die Komposition. Verwenden Sie DisposableEffect Wenn sich die DisposableEffect-Schlüssel ändern, muss die zusammensetzbare Funktion entfernt werden. der Bereinigung) und durch erneutes Aufrufen des Effekts zurückgesetzt wird.

So können Sie z. B. Analyseereignisse senden, die auf Lifecycle Ereignisse mit einer LifecycleObserver Wenn Sie in Compose auf diese Ereignisse warten möchten, registrieren Sie sich mit einem DisposableEffect und können Sie die Registrierung des Beobachters aufheben.

@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 */
}

Im Code oben wird durch den Effekt observer zum lifecycleOwner. Wenn sich lifecycleOwner ändert, wird der Effekt entfernt und mit der neuen lifecycleOwner neu gestartet.

Ein DisposableEffect muss eine onDispose-Klausel als letzte Anweisung enthalten. in seinem Codeblock. Andernfalls zeigt die IDE einen Build-Zeitfehler an.

SideEffect: Erstellungsstatus in Nicht-Compose-Code veröffentlichen

Wenn Sie den Status der Erstellung für Objekte freigeben möchten, die nicht vom Typ „Compose“ verwaltet werden, verwenden Sie den SideEffect zusammensetzbar. Mit einem SideEffect wird sichergestellt, dass der Effekt nach jedem erfolgreiche Neuzusammensetzung. Andererseits ist es falsch, bevor eine erfolgreiche Neuzusammensetzung garantiert wird. wenn Sie den Effekt direkt in eine zusammensetzbare Funktion schreiben.

Mithilfe der Analysebibliothek können Sie beispielsweise Nutzer in Segmente durch Hinzufügen benutzerdefinierter Metadaten (in diesem Beispiel „Nutzereigenschaften“). auf alle nachfolgenden Analyseereignisse anwenden. Um den Nutzertyp der aktuellen Nutzer zu Ihrer Analysebibliothek hinzu, verwenden Sie SideEffect, um den Wert zu aktualisieren.

@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: Status ohne Erstellung in einen Erstellungsstatus umwandeln

produceState startet eine Koroutine für die Komposition, die Werte in eine hat State zurückgegeben. Damit kannst du Konvertieren Sie den Status „Nicht erstellt“ in den Status „Verfassen“, z. B. mit externen abogesteuerten Status wie Flow, LiveData oder RxJava in der Komposition

Der Produzent wird gestartet, wenn produceState die Komposition betritt, und wird wird beim Verlassen der Komposition abgebrochen. Die zurückgegebene State-Anfrage führt zu einer Zusammenführung: wird keine Neuzusammensetzung ausgelöst.

Obwohl produceState eine Koroutine erstellt, kann sie auch verwendet werden, um zu beobachten, Datenquellen ohne Unterbrechung. Um das Abo für diese Quelle zu entfernen, verwenden Sie die awaitDispose .

Das folgende Beispiel zeigt, wie Sie mit produceState ein Bild aus dem Netzwerk. Die zusammensetzbare Funktion loadNetworkImage gibt einen State-Wert zurück, der die auch in anderen zusammensetzbaren Funktionen verwendet werden können.

@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: Konvertiert ein oder mehrere Zustandsobjekte in einen anderen Status

In „Compose“ erfolgt die Neuzusammensetzung jedes Mal, wenn sich ein beobachtetes Statusobjekt oder eine zusammensetzbare Eingabe ändert. Ein Zustandsobjekt ändern, als die Benutzeroberfläche tatsächlich aktualisiert werden muss, und führt zu einer unnötigen Neuzusammensetzung.

Sie sollten den derivedStateOf verwenden. , wenn sich die Eingaben für eine zusammensetzbare Funktion häufiger als nötig ändern um sich neu zusammenzusetzen. Das kommt häufig vor, wenn sich etwas häufig ändert, z. B. eine Scrollposition, aber die zusammensetzbare Funktion muss nur darauf reagieren, einen bestimmten Grenzwert erreichen. derivedStateOf erstellt ein neues Objekt für den Erstellungsstatus, dass sie nur so oft aktualisiert wird, wie Sie benötigen. Auf diese Weise ähnlich wie bei Kotlin-Datenflüssen distinctUntilChanged() .

Richtige Verwendung

Das folgende Snippet zeigt einen geeigneten Anwendungsfall für 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()
        }
    }
}

In diesem Snippet ändert sich firstVisibleItemIndex jedes Mal, wenn das erste sichtbare Element Änderungen. Wenn Sie scrollen, ändert sich der Wert zu 0, 1, 2, 3, 4, 5 usw. Eine Neuzusammensetzung muss jedoch nur erfolgen, wenn der Wert größer als 0 ist. Diese Abweichung bei der Aktualisierungshäufigkeit bedeutet, dass dies ein guter Anwendungsfall für derivedStateOf

Falsche Verwendung

Ein häufiger Fehler besteht darin, beim Kombinieren von zwei Compose-Objekten den Fehler sollten Sie derivedStateOf verwenden, da Sie einen "Ableitungsstatus" angeben. Dieses ist reiner Aufwand und nicht erforderlich, wie das folgende Snippet zeigt:

// 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

In diesem Snippet muss fullName genauso oft aktualisiert werden wie firstName und lastName Daher findet keine übermäßige Neuzusammensetzung statt. derivedStateOf ist nicht erforderlich.

snapshotFlow: Zustand von „Compose“ in Abläufe konvertieren

snapshotFlow verwenden zum Umrechnen von State<T> in einen kalten Flow verschieben. snapshotFlow führt seinen Block aus, wenn das Gerät erfasst und ausgegeben wird das Ergebnis der darin gelesenen State-Objekte. Wenn eines der State-Objekte snapshotFlow-Block mutate-Werte enthalten, gibt der Flow den neuen Wert aus an seinen Collector, wenn der neue Wert nicht gleich den vorherigen ausgegebenen Wert (dieses Verhalten ähnelt dem Flow.distinctUntilChanged)

Das folgende Beispiel zeigt einen Nebeneffekt, bei dem erfasst wird, wann der Nutzer scrollt das erste Element in einer Liste an Analytics übergeben:

val listState = rememberLazyListState()

LazyColumn(state = listState) {
    // ...
}

LaunchedEffect(listState) {
    snapshotFlow { listState.firstVisibleItemIndex }
        .map { index -> index > 0 }
        .distinctUntilChanged()
        .filter { it == true }
        .collect {
            MyAnalyticsService.sendScrolledPastFirstItemEvent()
        }
}

Im obigen Code wird listState.firstVisibleItemIndex in einen Ablauf konvertiert, der können von der Leistungsfähigkeit der Flow-Operatoren profitieren.

Effekte neu starten

Einige Effekte in „Schreiben“, z. B. LaunchedEffect, produceState oder DisposableEffect eine variable Anzahl von Argumenten, Schlüsseln, brechen Sie den laufenden Effekt ab und starten Sie einen neuen mit den neuen Schlüsseln.

Die typische Form für diese APIs lautet:

EffectName(restartIfThisKeyChanges, orThisKey, orThisKey, ...) { block }

Aufgrund der Feinheiten dieses Verhaltens können Probleme auftreten, wenn die Parameter die zum Neustart des Effekts verwendet wurden, nicht die richtigen sind:

  • Wenn ein Neustart weniger Auswirkungen hat, als er sollte, können Fehler in der App auftreten.
  • Ein Neustart kann ineffizient sein, als sie eigentlich sein sollten.

Als Faustregel gilt: Veränderliche und unveränderliche Variablen, die im Effektblock von sollte der zusammensetzbaren Funktion als Parameter hinzugefügt werden. Abgesehen davon können weitere Parameter hinzugefügt werden, um den Neustart des Effekts zu erzwingen. Wenn die Änderung von Eine Variable sollte den Effekt nicht neu starten. Die Variable sollte umschlossen werden. in rememberUpdatedState Wenn die Variable nie da es in eine remember ohne Schlüssel verpackt ist, müssen Sie Variable als Schlüssel für den Effekt übergeben.

Im oben gezeigten DisposableEffect-Code nimmt der Effekt als Parameter der lifecycleOwner wird in diesem Block verwendet, da jede Änderung dazu führen sollte, einen Effekt neu zu starten.

@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 und currentOnStop werden als DisposableEffect nicht benötigt Schlüssel, da ihr Wert sich bei der Zusammensetzung nie aufgrund der Verwendung von rememberUpdatedState. Wenn Sie lifecycleOwner nicht als Parameter übergeben und Es ändert sich, HomeScreen wird neu zusammengesetzt, DisposableEffect wird jedoch nicht entsorgt und neu gestartet. Das verursacht Probleme, da die falsche lifecycleOwner die ab diesem Zeitpunkt genutzt werden.

Konstanten als Schlüssel

Sie können eine Konstante wie true als Effektschlüssel verwenden, Dem Lebenszyklus der Anrufwebsite folgen Es gibt gültige Anwendungsfälle für wie im LaunchedEffect-Beispiel oben zu sehen. Zuvor sollten Sie jedoch überlegen Sie sich genau, ob das wirklich nötig ist.