Ein Nebeneffekt ist eine Änderung des App-Status, die außerhalb des Gültigkeitsbereichs einer kompositionsfähigen Funktion erfolgt. Aufgrund des Lebenszyklus und der Eigenschaften von Composeables wie unvorhersehbare Neuzusammensetzungen, die Ausführung von Neuzusammensetzungen von Composeables in unterschiedlichen Reihenfolgen oder Neuzusammensetzungen, die verworfen werden können, sollten Composeables idealerweise ohne Nebenwirkungen sein.
Manchmal sind jedoch Nebenwirkungen erforderlich, z. B. um ein einmaliges Ereignis auszulösen, wie das Einblenden einer Snackbar oder das Wechseln zu einem anderen Bildschirm bei einem bestimmten Status. Diese Aktionen sollten aus einer kontrollierten Umgebung aufgerufen werden, die den Lebenszyklus des Composeable kennt. Auf dieser Seite erfahren Sie mehr über die verschiedenen Side-Effect APIs, die Jetpack Compose bietet.
Zustand und Wirkung – Anwendungsfälle
Wie in der Dokumentation Mit Compose denken beschrieben, sollten Compose-Elemente keine Nebenwirkungen haben. Wenn Sie Änderungen am Status der Anwendung vornehmen müssen, wie in der Dokumentation zur Statusverwaltung beschrieben, sollten Sie die Effect APIs so verwenden, dass diese Nebenwirkungen vorhersehbar ausgeführt werden.
Aufgrund der verschiedenen Möglichkeiten, die sich in Compose bieten, können sie leicht überstrapaziert werden. Achten Sie darauf, dass die darin ausgeführten Arbeiten sich auf die Benutzeroberfläche beziehen und den einseitigen Datenfluss nicht unterbrechen, wie in der Dokumentation zum Verwalten des Status erläutert.
LaunchedEffect
: Beendigungsfunktionen im Bereich einer zusammensetzbaren Funktion ausführen
Wenn Sie über die gesamte Lebensdauer einer zusammensetzbaren Funktion arbeiten und Sperrungsfunktionen aufrufen möchten, verwenden Sie die zusammensetzbare Funktion LaunchedEffect
. Wenn LaunchedEffect
in die Zusammensetzung eintritt, startet es eine Koroutine mit dem als Parameter übergebenen Codeblock. Die Coroutine wird abgebrochen, wenn LaunchedEffect
die Komposition verlässt. Wenn LaunchedEffect
mit anderen Schlüsseln neu zusammengesetzt wird (siehe Abschnitt Neustart von Effekten unten), wird die vorhandene Coroutine abgebrochen und die neue Suspend-Funktion in einer neuen Coroutine gestartet.
Hier ist beispielsweise 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 Code oben wird die Funktion delay
verwendet, um die festgelegte Zeitspanne zu warten. Anschließend wird der Alphawert mit animateTo
nacheinander auf null und wieder zurück animiert.
Dies wird während der gesamten Lebensdauer des Composeables wiederholt.
rememberCoroutineScope
: einen zusammensetzungsbewussten Bereich abrufen, um eine Coroutine außerhalb eines Composeables zu starten
Da LaunchedEffect
eine zusammensetzbare Funktion ist, kann sie nur in anderen zusammensetzbaren Funktionen verwendet werden. Wenn Sie eine Coroutine außerhalb eines Composeables starten möchten, sie aber so eingrenzen möchten, 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 Koroutinen manuell steuern müssen, z. B. um eine Animation abzubrechen, wenn ein Nutzerereignis eintritt.
rememberCoroutineScope
ist eine kombinierbare 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önntest du mit diesem Code eine Snackbar
anzeigen lassen, 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
: Verweis 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 Fällen kann es jedoch sinnvoll sein, einen Wert in deinem Effekt zu erfassen, der den Effekt nicht neu starten soll, wenn er sich ändert. Dazu müssen Sie mit rememberUpdatedState
einen Verweis auf diesen Wert erstellen, der erfasst und aktualisiert werden kann. Dieser Ansatz eignet sich für Effekte mit langlebigen Vorgängen, deren Neuerstellung und Neustart kostspielig oder zu kostspielig sein kann.
Angenommen, Ihre App enthält eine LandingScreen
, die nach einiger Zeit verschwindet. Auch wenn LandingScreen
neu zusammengesetzt wird, sollte der Effekt, der eine Weile wartet und benachrichtigt, dass die Zeit verstrichen 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 als Parameter eine unveränderliche Konstante wie Unit
oder true
übergeben. Im obigen Code wird LaunchedEffect(true)
verwendet. Damit die Lambda-Funktion onTimeout
immer den neuesten Wert enthält, mit dem LandingScreen
neu zusammengesetzt wurde, muss onTimeout
in die Funktion rememberUpdatedState
eingekapselt werden.
Die zurückgegebenen State
, currentOnTimeout
im Code sollten im Effekt verwendet werden.
DisposableEffect
: Effekte, die bereinigt werden müssen
Verwenden Sie DisposableEffect
für Nebenwirkungen, die nach der Änderung der Schlüssel beseitigt werden müssen oder wenn das kompositionsfähige Element die Komposition verlässt.
Wenn sich die DisposableEffect
-Schlüssel ändern, muss das Composeable den aktuellen Effekt entfernen (bereinigen) und durch erneutes Aufrufen des Effekts zurücksetzen.
So können Sie beispielsweise Analytics-Ereignisse basierend auf Lifecycle
-Ereignissen mithilfe eines LifecycleObserver
senden.
Wenn Sie in Compose auf diese Ereignisse warten möchten, verwenden Sie ein DisposableEffect
, um den Beobachter bei Bedarf zu registrieren und wieder abzumelden.
@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 die observer
der lifecycleOwner
hinzugefügt. Wenn sich lifecycleOwner
ändert, wird der Effekt entfernt und mit dem neuen lifecycleOwner
neu gestartet.
Ein DisposableEffect
muss in seinem Codeblock eine onDispose
-Klausel als letzte Anweisung enthalten. Andernfalls wird in der IDE ein Fehler bei der Buildzeit 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 den Befehl SideEffect
composable. Wenn du einen SideEffect
verwendest, wird der Effekt nach jeder erfolgreichen Neukomposition ausgeführt. Andererseits ist es falsch, einen Effekt auszuführen, bevor eine erfolgreiche Neuzusammensetzung garantiert ist. Das ist der Fall, wenn der Effekt direkt in ein Composeable geschrieben wird.
Mit Ihrer Analysebibliothek können Sie beispielsweise Ihre Nutzergruppe segmentieren, indem Sie allen nachfolgenden Analyseereignissen benutzerdefinierte Metadaten („Nutzereigenschaften“ in diesem Beispiel) zuordnen. Wenn Sie den Nutzertyp des aktuellen Nutzers an Ihre Analysebibliothek senden möchten, aktualisieren Sie den Wert mit 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
: Status ohne Erstellung in einen Erstellungsstatus umwandeln
produceState
startet eine coroutine, die auf die Komposition beschränkt ist und Werte in eine zurückgegebene State
einfügen kann. Mit dieser Funktion können Sie einen nicht Compose-Status in einen Compose-Status umwandeln, z. B. externen abobasierten Status wie Flow
, LiveData
oder RxJava
in die Komposition einbinden.
Der Producer wird gestartet, wenn produceState
die Komposition betritt, und annulliert, wenn er sie verlässt. Der zurückgegebene State
-Wert wird zusammengefasst. Wenn Sie denselben Wert festlegen, wird keine Neuzusammensetzung ausgelöst.
Obwohl produceState
eine Koroutine erstellt, kann sie auch zum Beobachten nicht anhaltender Datenquellen verwendet werden. Verwenden Sie die Funktion awaitDispose
, 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 loadNetworkImage
-Funktion gibt eine State
zurück, die in anderen Composeables 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
: Konvertiert ein oder mehrere Zustandsobjekte in einen anderen Status
In Compose erfolgt eine Neuzusammensetzung jedes Mal, wenn sich ein beobachtetes Statusobjekt oder eine zusammensetzbare Eingabe ändert. Ein Statusobjekt oder eine Statuseingabe kann sich häufiger ändern, als die UI tatsächlich aktualisiert werden muss, was zu einer unnötigen Neuzusammensetzung führt.
Sie sollten die Funktion derivedStateOf
verwenden, wenn sich die Eingaben für eine zusammensetzbare Funktion häufiger ändern, als Sie sie neu zusammensetzen müssen. Dies tritt häufig auf, wenn sich etwas häufig ändert, z. B. bei einer Scrollposition, aber die zusammensetzbare Funktion muss erst darauf reagieren, wenn sie einen bestimmten Grenzwert überschreitet. Mit derivedStateOf
wird ein neues Erstellungsstatusobjekt erstellt, das nur so oft aktualisiert wird, wie Sie benötigen. In dieser Hinsicht funktioniert er ähnlich wie der Operator distinctUntilChanged()
in 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 ändert sich der Wert in 0
, 1
, 2
, 3
, 4
, 5
usw. Die 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
ist.
Falsche Verwendung
Ein häufiger Fehler besteht darin, anzunehmen, dass Sie beim Kombinieren von zwei Compose-Zustandsobjekten derivedStateOf
verwenden sollten, weil Sie einen "Ableiten des Zustands" vornehmen. Dies ist jedoch reines Overhead 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 kommt es nicht zu einer übermäßigen Neuzusammensetzung und die Verwendung von derivedStateOf
ist nicht erforderlich.
snapshotFlow
: Zustand von „Compose“ in Abläufe konvertieren
Verwenden Sie snapshotFlow
, um State<T>
-Objekte in einen kalten Ablauf 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, mutiert, gibt der Ablauf den neuen Wert an den entsprechenden Collector aus, wenn er sich von dem zuvor ausgegeben Wert unterscheidet. Dieses Verhalten ähnelt dem von Flow.distinctUntilChanged
.
Im folgenden Beispiel wird ein Nebeneffekt gezeigt, bei dem erfasst wird, wenn der Nutzer in Analytics über den ersten Eintrag in einer Liste scrollt:
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 von den Funktionen der Flow-Operatoren profitieren kann.
Effekte neu starten
Einige Effekte in Compose, z. B. LaunchedEffect
, produceState
oder DisposableEffect
, nehmen eine variable Anzahl von Argumenten (Tasten) an, mit denen der laufende Effekt abgebrochen und ein neuer mit den neuen Tasten gestartet wird.
Das typische Format für diese APIs ist:
EffectName(restartIfThisKeyChanges, orThisKey, orThisKey, ...) { block }
Aufgrund der Feinheiten dieses Verhaltens können Probleme auftreten, wenn die Parameter, mit denen der Effekt neu gestartet wird, nicht richtig sind:
- Wenn die Auswirkungen des Neustarts geringer sind als sie sein sollten, 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: Veränderliche und unveränderliche Variablen, die im Effektblock des Codes verwendet werden, sollten dem Effekt-Composable als Parameter hinzugefügt werden. Außerdem können weitere Parameter hinzugefügt werden, um den Effekt zu erzwingen. Wenn die Änderung einer Variablen den Effekt nicht neu starten soll, sollte die Variable in rememberUpdatedState
eingeschlossen sein. Wenn sich die Variable nie ändert, weil sie in eine remember
ohne Schlüssel verpackt ist, müssen Sie die Variable nicht als Schlüssel für die Wirkung übergeben.
Im oben gezeigten DisposableEffect
-Code übernimmt der Effekt als Parameter von lifecycleOwner
, der in seinem Block verwendet wird, da jede Änderung an ihnen einen Neustart des Effekts zur Folge haben sollte.
@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 als DisposableEffect
-Schlüssel nicht erforderlich, da sich ihr Wert in der Komposition aufgrund der Verwendung von rememberUpdatedState
nie ändert. Wenn Sie lifecycleOwner
nicht als Parameter übergeben und dieser sich ändert, wird HomeScreen
neu zusammengesetzt, aber DisposableEffect
wird nicht entfernt und neu gestartet. Dies führt zu Problemen, da ab diesem Zeitpunkt die falsche lifecycleOwner
verwendet wird.
Konstanten als Schlüssel
Sie können eine Konstante wie true
als Effektschlüssel verwenden, damit der Effekt dem Lebenszyklus der Aufrufstelle folgt. Es gibt jedoch durchaus gültige Anwendungsfälle, wie das Beispiel LaunchedEffect
oben zeigt. Überlegen Sie sich jedoch vorher gut, ob Sie das wirklich brauchen.
Empfehlungen für dich
- Hinweis: Der Linktext wird angezeigt, wenn JavaScript deaktiviert ist.
- Zustand und Jetpack Compose
- Kotlin für Jetpack Compose
- Ansichten in der compose-Ansicht verwenden