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.
Anwendungsfälle für Status und Wirkung
Wie in der Dokumentation Mit Compose denken beschrieben, sollten Compose-Elemente keine Nebenwirkungen haben. Wenn Sie Änderungen am Status der App vornehmen müssen (wie in der Dokumentation zum Verwalten des Status beschrieben), sollten Sie die Effect APIs verwenden, damit 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
: Ausführen von Suspend-Funktionen im Rahmen einer komponierbaren Funktion
Wenn Sie während der Lebensdauer eines Composeables Aufgaben ausführen und Suspend-Funktionen aufrufen möchten, verwenden Sie das Composeable LaunchedEffect
. Wenn LaunchedEffect
die Komposition betritt, wird eine Coroutine mit dem Codeblock gestartet, der als Parameter übergeben wird. 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 kombinierbare Funktion ist, kann sie nur in anderen kombinierbaren 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 Tasks manuell steuern müssen, z. B. wenn eine Animation abgebrochen werden soll, wenn ein Nutzerereignis auftritt.
rememberCoroutineScope
ist eine kombinierbare Funktion, die ein CoroutineScope
zurückgibt, das an den Punkt der Komposition gebunden ist, an dem sie aufgerufen wird. Der Umfang wird aufgehoben, wenn der Anruf die Zusammenstellung 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 wichtigen Parameter ä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 muss mit rememberUpdatedState
eine Referenz auf diesen Wert erstellt werden, die erfasst und aktualisiert werden kann. Dieser Ansatz ist hilfreich für Effekte, die langlebige Vorgänge enthalten, deren Neuerstellung und Neustart teuer oder unzumutbar sein können.
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 vergangene Zeit nicht neu gestartet werden soll, 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 beseitigt 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 entsorgen (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 als letzte Anweisung in seinem Codeblock eine onDispose
-Klausel 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 Nutzerpopulation 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
: Nicht-Compose-Status in Compose-Status 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 zusammengeführt. Wenn Sie denselben Wert festlegen, wird keine Neuzusammensetzung ausgelöst.
Auch wenn produceState
eine Coroutine erstellt, kann sie auch zum Beobachten von nicht angehaltenen 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 Composeable-Elementen 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 umwandeln
In Compose erfolgt eine Neuzusammensetzung jedes Mal, 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 aktualisiert werden muss, was zu unnötigen Neuzusammensetzungen führt.
Sie sollten die Funktion derivedStateOf
verwenden, wenn sich die Eingaben für ein Composeable häufiger ändern, als es neu zusammengesetzt werden muss. Das ist oft der Fall, wenn sich etwas häufig ändert, z. B. eine Scrollposition, das Composeable aber nur reagieren muss, wenn ein bestimmter Grenzwert überschritten wird. derivedStateOf
erstellt ein neues „Compose State“-Objekt, das Sie beobachten können und das nur so oft aktualisiert wird, wie Sie es 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 ist die Annahme, dass Sie beim Kombinieren von zwei Compose-Statusobjekten derivedStateOf
verwenden sollten, weil Sie den Status „ableiten“. 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
: Compose-Status in Abläufe umwandeln
Verwenden Sie snapshotFlow
, um State<T>
-Objekte in einen kalten Ablauf umzuwandeln. 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 nicht dazu führen soll, dass der Effekt neu gestartet wird, sollte die Variable in rememberUpdatedState
eingeschlossen werden. Wenn sich die Variable nie ändert, weil sie in einen remember
ohne Schlüssel verpackt ist, müssen Sie sie nicht als Schlüssel an den Effekt übergeben.
Im obigen DisposableEffect
-Code nimmt der Effekt als Parameter den lifecycleOwner
aus seinem Block an, 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 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 sich dieser ändert, wird HomeScreen
neu erstellt, aber DisposableEffect
wird nicht verworfen und neu gestartet. Das führt zu Problemen, da ab diesem Punkt 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 aber 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