Ein Nebeneffekt ist eine Änderung des App-Status, die außerhalb des Bereichs einer komponierbaren Funktion erfolgt. Aufgrund des Lebenszyklus und der Eigenschaften von Composables wie unvorhersehbare Neukompositionen, Neukompositionen von Composables in unterschiedlicher Reihenfolge oder Neukompositionen, die verworfen werden können, sollten Composables idealerweise keine Nebeneffekte haben.
Manchmal sind Nebeneffekte jedoch erforderlich, um beispielsweise ein einmaliges Ereignis auszulösen, z. B. die Anzeige einer Snackbar oder die Navigation zu einem anderen Bildschirm bei einer bestimmten Statusbedingung. Diese Aktionen sollten aus einer kontrollierten Umgebung aufgerufen werden, die den Lebenszyklus des Composables kennt. Auf dieser Seite erfahren Sie mehr über die verschiedenen APIs für Nebeneffekte, die Jetpack Compose bietet.
Anwendungsfälle für Status und Effekte
Wie in der Thinking in Compose Dokumentation beschrieben, sollten Composables keine Nebeneffekte haben. Wenn Sie Änderungen am Status der App vornehmen müssen (wie im Dokument Status verwalten beschrieben), sollten Sie die Effect APIs verwenden, damit diese Nebeneffekte auf vorhersehbare Weise ausgeführt werden.
Aufgrund der verschiedenen Möglichkeiten, die Effekte in Compose bieten, können sie leicht überbeansprucht werden. Achten Sie darauf, dass die Arbeit, die Sie darin ausführen, UI-bezogen ist und den unidirektionalen Datenfluss nicht unterbricht, wie in der Dokumentation Status verwalten erläutert.
LaunchedEffect: Suspend-Funktionen im Bereich eines Composables ausführen
Wenn Sie während des Lebenszyklus eines Composables arbeiten und Suspend-Funktionen aufrufen möchten, verwenden Sie das
LaunchedEffect
Composable. Wenn LaunchedEffect in die Komposition eintritt, wird eine Coroutine mit dem als Parameter übergebenen Codeblock gestartet. Die Coroutine wird abgebrochen, wenn LaunchedEffect die Komposition verlässt. Wenn LaunchedEffect mit
anderen Schlüsseln neu zusammengesetzt wird (siehe Abschnitt Effekte
neu starten unten), wird die vorhandene Coroutine abgebrochen und die neue Suspend-Funktion wird in einer neuen Coroutine gestartet.
Hier ist ein Beispiel für eine Animation, die den Alphawert mit einer konfigurierbaren Verzögerung pulsiert:
// 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) } }
Im obigen Code verwendet die Animation die Suspend-Funktion
delay
um die festgelegte Zeit zu warten. Anschließend wird der Alphawert
mit
animateTo sequenziell auf null und wieder zurück animiert.
Dieser Vorgang wird während des gesamten Lebenszyklus des Composables wiederholt.
rememberCoroutineScope: Einen kompositionsbezogenen Bereich abrufen, um eine Coroutine außerhalb eines Composables zu starten
LaunchedEffect ist eine komponierbare Funktion und kann daher nur innerhalb anderer komponierbarer Funktionen verwendet werden. Wenn Sie eine Coroutine außerhalb eines Composables starten möchten,
aber so, dass sie automatisch abgebrochen wird, sobald sie die
Komposition verlässt, verwenden Sie
rememberCoroutineScope.
Verwenden Sie rememberCoroutineScope auch, wenn Sie den Lebenszyklus einer oder mehrerer Coroutinen manuell steuern müssen, z. B. um eine Animation abzubrechen, wenn ein Nutzerereignis eintritt.
rememberCoroutineScope ist eine komponierbare Funktion, die ein
CoroutineScope zurückgibt, das an den Punkt der Komposition gebunden ist, an dem sie aufgerufen wird. Der Bereich wird abgebrochen, wenn der Aufruf die Komposition verlässt.
Im Anschluss an das vorherige Beispiel können Sie diesen Code verwenden, um eine Snackbar anzuzeigen, wenn der Nutzer auf eine 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: Auf einen Wert in einem Effekt verweisen, der nicht neu gestartet werden soll, wenn sich der Wert ändert
LaunchedEffect wird neu gestartet, wenn sich einer der Schlüsselparameter ändert. In einigen Fällen möchten Sie jedoch möglicherweise einen Wert in Ihrem Effekt erfassen, bei dem der Effekt nicht neu gestartet werden soll, wenn er sich ändert. Dazu müssen Sie rememberUpdatedState verwenden, um einen Verweis auf diesen Wert zu erstellen, der erfasst und aktualisiert werden kann. Dieser Ansatz ist hilfreich für Effekte, die langwierige Vorgänge enthalten, die teuer oder unmöglich neu zu erstellen und neu zu starten sind.
Angenommen, Ihre App hat einen LandingScreen, der nach einiger Zeit verschwindet. Auch wenn LandingScreen neu zusammengesetzt wird, sollte der Effekt, der einige Zeit wartet und benachrichtigt, dass die Zeit abgelaufen ist, nicht neu gestartet werden:
@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 erstellen, der dem Lebenszyklus der Aufrufstelle entspricht, wird eine unveränderliche Konstante wie Unit oder true als Parameter übergeben. Im obigen Code wird LaunchedEffect(true) verwendet. Damit die onTimeout-Lambda-Funktion immer den neuesten Wert enthält, mit dem LandingScreen neu zusammengesetzt wurde, muss onTimeout mit der Funktion rememberUpdatedState umschlossen werden.
Der zurückgegebene State, currentOnTimeout im Code, sollte im Effekt verwendet werden.
DisposableEffect: Effekte, die bereinigt werden müssen
Verwenden SieDisposableEffect für Nebeneffekte, die bereinigt werden müssen, nachdem sich die Schlüssel geändert haben oder wenn das
Composable die Komposition verlässt.
Wenn sich die DisposableEffect-Schlüssel ändern, muss die komponierbare Funktion ihren aktuellen Effekt verwerfen (bereinigen) und zurücksetzen, indem sie den Effekt noch einmal aufruft.
Sie können beispielsweise Analysedaten basierend auf
Lifecycle Ereignissen
mit einem
LifecycleObserver senden.
Wenn Sie in Compose auf diese Ereignisse warten möchten, verwenden Sie DisposableEffect, um den Observer bei Bedarf zu registrieren und die Registrierung aufzuheben.
@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 obigen Code fügt der Effekt den observer zum lifecycleOwner hinzu. Wenn sich lifecycleOwner ändert, wird der Effekt verworfen und mit dem neuen lifecycleOwner neu gestartet.
Ein DisposableEffect muss eine onDispose-Klausel als letzte Anweisung im Codeblock enthalten. Andernfalls zeigt die IDE einen Build-Zeit-Fehler an.
SideEffect: Compose-Status in Nicht-Compose-Code veröffentlichen
Wenn Sie den Compose-Status für Objekte freigeben möchten, die nicht von Compose verwaltet werden, verwenden Sie das
SideEffect
-Composable. Mit SideEffect wird garantiert, dass der Effekt nach jeder
erfolgreichen Neukomposition ausgeführt wird. Andererseits ist es falsch, einen Effekt auszuführen, bevor eine erfolgreiche Neukomposition garantiert ist. Das ist der Fall, wenn Sie den Effekt direkt in ein Composable schreiben.
Mit Ihrer Analysebibliothek können Sie beispielsweise Ihre Nutzer nach Segmenten aufteilen, indem Sie allen nachfolgenden Analyseereignissen benutzerdefinierte Metadaten anhängen (in diesem Beispiel „Nutzerattribute“). Wenn Sie den Nutzertyp des aktuellen Nutzers an Ihre Analysebibliothek senden möchten, 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: Nicht-Compose-Status in Compose-Status konvertieren
produceState
startet eine Coroutine, die auf die Komposition beschränkt ist und Werte in einen
zurückgegebenen Stateübertragen kann. Verwenden Sie sie, um Nicht-Compose-Status in Compose-Status zu konvertieren, z. B. um externe abobasierte Status wie Flow, LiveData oder RxJava in die Komposition zu übertragen.
Der Producer wird gestartet, wenn produceState in die Komposition eintritt, und wird abgebrochen, wenn er die Komposition verlässt. Der zurückgegebene State wird zusammengeführt. Wenn Sie denselben Wert festlegen, wird keine Neukomposition ausgelöst.
Obwohl produceState eine Coroutine erstellt, kann sie auch verwendet werden, um nicht suspendierende Datenquellen zu beobachten. Verwenden Sie
die
awaitDispose
Funktion, um das Abo für diese Quelle zu entfernen.
Im folgenden Beispiel wird gezeigt, wie Sie mit produceState ein Bild aus dem Netzwerk laden. Die komponierbare Funktion loadNetworkImage gibt einen State zurück, der in anderen Composables verwendet werden kann.
@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: Ein oder mehrere Statusobjekte in einen anderen Status konvertieren
In Compose, die Neukomposition erfolgt jedes Mal, wenn sich ein beobachtetes Statusobjekt oder eine komponierbare Eingabe ändert. Ein Statusobjekt oder eine Eingabe ändert sich möglicherweise häufiger als die UI tatsächlich aktualisiert werden muss, was zu unnötigen Neukompositionen führt.
Verwenden Sie die derivedStateOf
Funktion, wenn sich die Eingaben für ein Composable häufiger ändern, als Sie es neu zusammensetzen müssen. Das ist oft der Fall, wenn sich etwas häufig ändert, z. B. eine Scrollposition, das Composable aber erst reagieren muss, wenn ein bestimmter Schwellenwert überschritten wird. derivedStateOf erstellt ein neues Compose-Statusobjekt, das Sie beobachten können und das nur so oft aktualisiert wird, wie Sie es benötigen. In dieser Hinsicht ähnelt es dem Operator
von Kotlin Flows
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 sich das erste sichtbare Element ändert. Beim Scrollen wird der Wert zu 0, 1, 2, 3, 4, 5 usw. Eine Neukomposition ist jedoch nur erforderlich, wenn der Wert größer als 0 ist.
Diese Diskrepanz in der Updatehäufigkeit macht dies zu einem guten Anwendungsfall für derivedStateOf.
Falsche Verwendung
Ein häufiger Fehler ist die Annahme, dass Sie derivedStateOf verwenden sollten, wenn Sie zwei Compose-Statusobjekte kombinieren, weil Sie „Status ableiten“. Das ist jedoch reiner Overhead und nicht erforderlich, wie im folgenden Snippet gezeigt:
// 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 kommt es zu keiner übermäßigen Neukomposition und die Verwendung von derivedStateOf ist nicht erforderlich.
snapshotFlow: Compose-Status in Flows konvertieren
Verwenden Sie snapshotFlow
zum Konvertieren von State<T>
-Objekten in einen Cold Flow. snapshotFlow führt seinen Block aus, wenn er erfasst wird, und gibt das Ergebnis der darin gelesenen State-Objekte aus. Wenn sich eines der State Objekte
ändert, die im snapshotFlow Block gelesen wurden, gibt der Flow den neuen Wert
an seinen Collector aus, wenn der neue Wert nicht mit
dem zuvor ausgegebenen Wert übereinstimmt. Dieses Verhalten ähnelt dem von
Flow.distinctUntilChanged).
Im folgenden Beispiel wird ein Nebeneffekt gezeigt, der erfasst, wenn der Nutzer in einer Liste am ersten Element vorbeiscrollt, und die Daten an die Analyse sendet:
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 Flow konvertiert, der die Leistungsfähigkeit der Flow-Operatoren nutzen kann.
Effekte neu starten
Einige Effekte in Compose, z. B. LaunchedEffect, produceState oder DisposableEffect, verwenden eine variable Anzahl von Argumenten, Schlüsseln, die verwendet werden, um den laufenden Effekt abzubrechen und einen neuen mit den neuen Schlüsseln zu starten.
Die typische Form für diese APIs ist:
EffectName(restartIfThisKeyChanges, orThisKey, orThisKey, ...) { block }
Aufgrund der Feinheiten dieses Verhaltens können Probleme auftreten, wenn die Parameter, die zum Neustarten des Effekts verwendet werden, nicht die richtigen sind:
- Wenn Effekte seltener als nötig neu gestartet werden, kann das zu Fehlern in Ihrer App führen.
- Wenn Effekte häufiger als nötig neu gestartet werden, kann das ineffizient sein.
Als Faustregel gilt, dass veränderliche und unveränderliche Variablen, die im Effektblock des Codes verwendet werden, als Parameter zum Effekt-Composable hinzugefügt werden sollten. Außerdem können weitere Parameter hinzugefügt werden, um den Effekt neu zu starten. Wenn die Änderung von
einer Variablen nicht dazu führen soll, dass der Effekt neu gestartet wird, sollte die Variable
in rememberUpdatedState umschlossen werden. Wenn sich die Variable nie ändert, weil sie in remember ohne Schlüssel umschlossen ist, müssen Sie die Variable nicht als Schlüssel an den Effekt übergeben.
Im obigen DisposableEffect-Code verwendet der Effekt den lifecycleOwner, der in seinem Block verwendet wird, als Parameter, da jede Änderung daran dazu führen sollte, dass der Effekt neu gestartet wird.
@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 sind nicht als DisposableEffect-Schlüssel erforderlich, da sich ihr Wert in der Komposition aufgrund der Verwendung von rememberUpdatedState nie ändert. Wenn Sie lifecycleOwner nicht als Parameter übergeben und er sich ändert, wird HomeScreen neu zusammengesetzt, aber DisposableEffect wird nicht verworfen und neu gestartet. Das führt zu Problemen, da ab diesem Zeitpunkt der falsche lifecycleOwner verwendet wird.
Konstanten als Schlüssel
Sie können eine Konstante wie true als Effektschlüssel verwenden, damit sie dem Lebenszyklus der Aufrufstelle folgt. Es gibt gültige Anwendungsfälle dafür, z. B. das oben gezeigte LaunchedEffect-Beispiel. Überlegen Sie sich jedoch vorher gut, ob das wirklich das ist, was Sie brauchen.
Empfehlungen für Sie
- Hinweis: Linktext wird angezeigt, wenn JavaScript deaktiviert ist
- Status und Jetpack Compose
- Kotlin für Jetpack Compose
- Ansichten in Compose verwenden