Ein Nebeneffekt ist eine Änderung des App-Status, die außerhalb des Bereichs einer zusammensetzbaren Funktion erfolgt. Aufgrund des Lebenszyklus von Composables und Eigenschaften wie unvorhersehbaren 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, z. B. um ein einmaliges Ereignis auszulösen, etwa die Anzeige einer Snackbar oder die Navigation zu einem anderen Bildschirm unter einer bestimmten Zustandsbedingung. Diese Aktionen sollten aus einer kontrollierten Umgebung aufgerufen werden, in der der Lebenszyklus des Composables bekannt ist. Auf dieser Seite erfahren Sie mehr über die verschiedenen Side-Effect-APIs, die Jetpack Compose bietet.
Anwendungsfälle für Status und Wirkung
Wie in der Dokumentation Thinking in Compose beschrieben, sollten Composables keine Nebeneffekte haben. Wenn Sie den Status der App ändern müssen (wie in der Dokumentation zur Statusverwaltung 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 überstrapaziert werden. Achten Sie darauf, dass die Aufgaben, die Sie in ihnen ausführen, mit der Benutzeroberfläche zusammenhängen und den unidirektionalen Datenfluss nicht unterbrechen, wie in der Dokumentation zum Verwalten des Status beschrieben.
LaunchedEffect
: Suspend-Funktionen im Bereich einer Komponierbaren ausführen
Wenn Sie während der Lebensdauer eines Composables Aufgaben ausführen 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 in einer neuen Coroutine gestartet.
Hier ist ein Beispiel für eine Animation, bei der der 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 { 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 wird die suspendierende Funktion delay
verwendet, um die festgelegte Zeit zu warten. Anschließend wird der Alphawert mit animateTo
sequenziell auf null und wieder zurück animiert.
Das wird für die gesamte Lebensdauer der Komponente wiederholt.
rememberCoroutineScope
: Einen kompositionsbezogenen Bereich abrufen, um eine Coroutine außerhalb einer Composable zu starten
Da LaunchedEffect
eine zusammensetzbare Funktion ist, kann sie nur innerhalb anderer zusammensetzbarer 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. wenn Sie eine Animation abbrechen möchten, wenn ein Nutzerereignis eintritt.
rememberCoroutineScope
ist eine zusammensetzbare 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 Anruf die Komposition verlässt.
Im Anschluss an das vorherige Beispiel können Sie mit diesem Code ein Snackbar
anzeigen, wenn der Nutzer auf ein 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 soll, wenn sich der Wert ändert.
LaunchedEffect
wird neu gestartet, wenn sich einer der Schlüsselparameter ändert. In einigen Situationen 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 lang andauernde Vorgänge enthalten, die teuer oder unmöglich zu reproduzieren und neu zu starten sind.
Angenommen, Ihre App hat ein LandingScreen
, das nach einiger Zeit verschwindet. Auch wenn LandingScreen
neu zusammengesetzt wird, sollte der Effekt, der einige Zeit wartet und dann 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 immer den neuesten Wert enthält, mit dem LandingScreen
neu zusammengesetzt wurde, muss onTimeout
mit der Funktion rememberUpdatedState
umschlossen werden.
Die zurückgegebenen State
und currentOnTimeout
im Code sollten im Effekt verwendet werden.
DisposableEffect
: Effekte, die bereinigt werden müssen
Verwenden Sie DisposableEffect
für Nebeneffekte, die bereinigt werden müssen, nachdem sich die Schlüssel geändert haben oder wenn die zusammensetzbare Funktion die Komposition verlässt.
Wenn sich die DisposableEffect
-Schlüssel ändern, muss die Composable ihren aktuellen Effekt verwerfen (die Bereinigung durchführen) und durch erneutes Aufrufen des Effekts zurückgesetzt werden.
Beispielsweise können Sie Analyseereignisse basierend auf Lifecycle
-Ereignissen mit einem LifecycleObserver
senden.
Wenn Sie in Compose auf diese Ereignisse warten möchten, verwenden Sie ein 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 wird durch den Effekt observer
zu lifecycleOwner
hinzugefügt. Wenn sich lifecycleOwner
ändert, wird der Effekt verworfen und mit dem neuen lifecycleOwner
neu gestartet.
Ein DisposableEffect
muss eine onDispose
-Anweisung als letzte Anweisung im Codeblock enthalten. Andernfalls wird in der IDE ein Build-Zeit-Fehler angezeigt.
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 die Composable SideEffect
. Die Verwendung von SideEffect
garantiert, dass der Effekt nach jeder erfolgreichen Neuzusammenstellung ausgeführt wird. Andererseits ist es falsch, einen Effekt auszuführen, bevor eine erfolgreiche Neuzusammensetzung garantiert ist. Das ist der Fall, wenn der Effekt direkt in eine zusammensetzbare Funktion geschrieben wird.
Mit Ihrer Analysebibliothek können Sie Ihre Nutzer beispielsweise segmentieren, indem Sie allen nachfolgenden Analyseereignissen benutzerdefinierte Metadaten (in diesem Beispiel „Nutzereigenschaften“) anhängen. Wenn Sie den Nutzertyp des aktuellen Nutzers an Ihre Analysenbibliothek übermitteln 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
einfügen kann. Damit können Sie Nicht-Compose-Status in Compose-Status umwandeln, z. B. externe abobasierte Status wie Flow
, LiveData
oder RxJava
in die Komposition einbringen.
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 Neuzusammensetzung ausgelöst.
Obwohl produceState
eine Coroutine erstellt, kann sie auch verwendet werden, um nicht suspendierende Datenquellen zu beobachten. Wenn Sie das Abo für diese Quelle entfernen möchten, verwenden Sie die Funktion awaitDispose
.
Im folgenden Beispiel wird gezeigt, wie Sie mit produceState
ein Bild aus dem Netzwerk laden. Die zusammensetzbare Funktion loadNetworkImage
gibt ein State
zurück, das in anderen zusammensetzbaren Funktionen 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 findet Recomposition jedes Mal statt, wenn sich ein beobachtetes Statusobjekt oder eine zusammensetzbare Eingabe ändert. Ein Statusobjekt oder eine Eingabe ändert sich möglicherweise häufiger als die Benutzeroberfläche tatsächlich aktualisiert werden muss, was zu unnötigen Neukompositionen führt.
Die Funktion derivedStateOf
sollten Sie verwenden, wenn sich die Eingaben für eine zusammensetzbare Funktion häufiger ändern, als Sie eine Neuzusammensetzung benötigen. 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-Zustandsobjekt, das Sie beobachten können und das nur so oft aktualisiert wird, wie nötig. Sie funktioniert ähnlich wie der distinctUntilChanged()
-Operator von Kotlin Flows.
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 Neuzusammensetzung ist jedoch nur erforderlich, wenn der Wert größer als 0
ist.
Diese Diskrepanz bei der Aktualisierungshäufigkeit macht derivedStateOf
zu einem guten Anwendungsfall.
Falsche Verwendung
Ein häufiger Fehler ist die Annahme, dass Sie beim Kombinieren von zwei Compose-Zustandsobjekten derivedStateOf
verwenden sollten, weil Sie „Zustand ableiten“. Dies ist jedoch nur zusätzlicher Aufwand und nicht erforderlich, wie im folgenden Snippet zu sehen ist:
// 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 ist keine übermäßige Neuzusammensetzung erforderlich und die Verwendung von derivedStateOf
ist nicht notwendig.
snapshotFlow
: Compose-Status in Flows umwandeln
Verwenden Sie snapshotFlow
, um State<T>
-Objekte in einen Cold Flow zu konvertieren. snapshotFlow
führt seinen Block aus, wenn er erfasst wird, und gibt das Ergebnis der darin gelesenen State
-Objekte aus. Wenn eines der State
-Objekte, die im snapshotFlow
-Block gelesen werden, geändert wird, gibt der Flow den neuen Wert an seinen Collector aus, wenn der neue Wert nicht gleich dem zuvor ausgegebenen Wert ist. Dieses Verhalten ähnelt dem von Flow.distinctUntilChanged
.
Im folgenden Beispiel wird ein Nebeneffekt gezeigt, der protokolliert, wenn der Nutzer in einer Liste am ersten Element vorbeiscrollt:
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 umgewandelt, 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üssel), mit denen der laufende Effekt abgebrochen und ein neuer Effekt mit den neuen Schlüsseln gestartet wird.
Die typische Form für diese APIs ist:
EffectName(restartIfThisKeyChanges, orThisKey, orThisKey, ...) { block }
Aufgrund der Feinheiten dieses Verhaltens können Probleme auftreten, wenn die zum Neustarten des Effekts verwendeten Parameter nicht die richtigen sind:
- Wenn Effekte weniger oft neu gestartet werden als vorgesehen, kann das zu Fehlern in Ihrer App führen.
- Das erneute Starten von Effekten kann 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. Abgesehen davon können weitere Parameter hinzugefügt werden, um den Effekt neu zu starten. Wenn durch die Änderung einer Variablen der Effekt nicht neu gestartet werden soll, muss die Variable in rememberUpdatedState
eingeschlossen werden. Wenn sich die Variable nie ändert, weil sie in einem remember
ohne Schlüssel enthalten ist, müssen Sie die Variable nicht als Schlüssel an den Effekt übergeben.
Im oben gezeigten DisposableEffect
-Code verwendet der Effekt die lifecycleOwner
in seinem Block als Parameter, da jede Änderung an ihnen 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 „Composition“ aufgrund der Verwendung von rememberUpdatedState
nie ändert. Wenn Sie lifecycleOwner
nicht als Parameter übergeben und sich der Wert ändert, wird HomeScreen
neu zusammengesetzt, aber DisposableEffect
wird nicht verworfen und neu gestartet. Das führt zu Problemen, weil ab diesem Zeitpunkt die falsche lifecycleOwner
verwendet wird.
Konstanten als Schlüssel
Sie können eine Konstante wie true
als Effekt-Schlüssel verwenden, damit sie dem Lebenszyklus der Aufrufstelle folgt. Es gibt gültige Anwendungsfälle dafür, wie das LaunchedEffect
-Beispiel oben zeigt. Bevor Sie das tun, sollten Sie jedoch noch einmal darüber nachdenken, ob Sie das wirklich benötigen.
Empfehlungen für dich
- Hinweis: Linktext wird angezeigt, wenn JavaScript deaktiviert ist.
- Zustand und Jetpack Compose
- Kotlin für Jetpack Compose
- Ansichten in Compose verwenden