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 zusammensetzbaren Funktionen wie unvorhersehbaren Neuzusammensetzungen, der Ausführung von Neuzusammensetzungen von Zusammensetzungen in unterschiedlicher Reihenfolge oder Neuzusammensetzungen, die verworfen werden können, sollten Zusammensetzungen im Idealfall keine Nebeneffekte haben.
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 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 Koroutine wird abgebrochen, wenn LaunchedEffect
die Zusammensetzung 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 zusammensetzbare Funktion, die eine CoroutineScope
zurückgibt, die an den Punkt der Komposition gebunden ist, an der 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 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. Selbst wenn LandingScreen
neu zusammengesetzt wird, wird der Effekt, der eine Zeit wartet und benachrichtigt, dass die vergangene Zeit nicht neu gestartet werden soll:
@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 Code oben 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.
Beispielsweise können Sie mithilfe von LifecycleObserver
Analyseereignisse senden, die auf Lifecycle
-Ereignissen basieren.
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 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
: 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 zusammengefasst. 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 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
: Ein oder mehrere Statusobjekte in einen anderen Status umwandeln
Bei der Zusammensetzung erfolgt jedes Mal eine Neuzusammensetzung, wenn sich ein beobachtetes Zustandsobjekt 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 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 Composed Element aber erst 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. Auf diese Weise funktioniert sie ähnlich wie der Kotlin-Operator distinctUntilChanged()
für Abläufe.
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 findet keine übermäßige Neuzusammensetzung statt 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 im snapshotFlow
-Block gelesenen State
-Objekte mutiert, gibt der Ablauf den neuen Wert an seinen Collector aus, wenn der neue Wert nicht gleich dem vorherigen ausgegebenen Wert ist (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 ein Neustart weniger Auswirkungen hat, als er sollte, können Fehler in der App auftreten.
- 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 eine remember
ohne Schlüssel verpackt ist, müssen Sie die Variable nicht als Schlüssel für die Wirkung ü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 jedoch vorher genau,
ob das wirklich nötig ist.
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