Ein Nebeneffekt ist eine Änderung des App-Status, die außerhalb des Umfangs einer zusammensetzbaren Funktion erfolgt. Aufgrund des Lebenszyklus und der Eigenschaften von zusammensetzbaren Funktionen wie unvorhersehbaren Neuzusammensetzungen, der Ausführung von Neuzusammensetzungen in unterschiedlicher Reihenfolge oder aufgrund von Neuzusammensetzungen, die verworfen werden können, sollten diese im Idealfall frei von Nebeneffekten sein.
Manchmal sind jedoch Nebeneffekte erforderlich, z. B. um ein einmaliges Ereignis auszulösen, z. B. das Anzeigen einer Snackbar oder das Aufrufen eines anderen Bildschirms unter bestimmten Bedingungen. Diese Aktionen sollten über eine kontrollierte Umgebung aufgerufen werden, in der der Lebenszyklus der zusammensetzbaren Funktion bekannt ist. Auf dieser Seite erfahren Sie mehr über die verschiedenen APIs mit Nebeneffekten, die Jetpack Compose bietet.
Anwendungsfälle und Auswirkungen
Wie in der Dokumentation In Compose erläutert, sollten zusammensetzbare Funktionen frei von Nebeneffekten sein. Wenn Sie den Status der Anwendung ändern müssen (wie in der Dokumentation zur Verwaltung des Status beschrieben), sollten Sie die Effect APIs verwenden, damit diese Nebeneffekte auf vorhersehbare Weise ausgeführt werden.
Aufgrund der verschiedenen Effekte, die sich in der Funktion „Compose“ ergeben, können sie leicht zu stark genutzt werden. Achten Sie darauf, dass sie sich auf die Benutzeroberfläche beziehen und den unidirektionalen Datenfluss nicht beeinträchtigen, wie in der Dokumentation zum Verwalten von Status erläutert.
LaunchedEffect
: zum Ausführen von Aussetzungsfunktionen im Bereich einer zusammensetzbaren Funktion
Verwenden Sie die zusammensetzbare Funktion LaunchedEffect
, um Sperrfunktionen sicher aus einer zusammensetzbaren Funktion aufzurufen. Wenn LaunchedEffect
in die Zusammensetzung eintritt, wird eine Koroutine mit dem Codeblock gestartet, der als Parameter übergeben wird. Die Koroutine wird abgebrochen, wenn LaunchedEffect
die Zusammensetzung verlässt. Wenn LaunchedEffect
mit anderen Tasten neu zusammengesetzt wird (siehe Abschnitt Neustarts von Effekten unten), wird die vorhandene Koroutine abgebrochen und die neue Anhaltefunktion in einer neuen Koroutine gestartet.
Wenn zum Beispiel ein Snackbar
in einem Scaffold
angezeigt wird, erfolgt die Darstellung mit der SnackbarHostState.showSnackbar
-Funktion, bei der es sich um eine Anhalten-Funktion handelt.
@Composable fun MyScreen( state: UiState<List<Movie>>, snackbarHostState: SnackbarHostState ) { // If the UI state contains an error, show snackbar if (state.hasError) { // `LaunchedEffect` will cancel and re-launch if // `scaffoldState.snackbarHostState` changes LaunchedEffect(snackbarHostState) { // Show snackbar using a coroutine, when the coroutine is cancelled the // snackbar will automatically dismiss. This coroutine will cancel whenever // `state.hasError` is false, and only start when `state.hasError` is true // (due to the above if-check), or if `scaffoldState.snackbarHostState` changes. snackbarHostState.showSnackbar( message = "Error message", actionLabel = "Retry message" ) } } Scaffold( snackbarHost = { SnackbarHost(hostState = snackbarHostState) } ) { contentPadding -> // ... } }
Im Code oben wird eine Koroutine ausgelöst, wenn der Status einen Fehler enthält. Andernfalls wird sie abgebrochen. Da sich die LaunchedEffect
-Aufrufwebsite in einer if-Anweisung befindet, wird die Anweisung, die auf "false" gesetzt ist, entfernt, wenn LaunchedEffect
in der Komposition enthalten war. Daher wird die Koroutine abgebrochen.
rememberCoroutineScope
: Rufen Sie einen Bereich ab, bei dem die Zusammensetzung berücksichtigt wird, um eine Koroutine außerhalb einer zusammensetzbaren Funktion zu starten.
Da LaunchedEffect
eine zusammensetzbare Funktion ist, kann sie nur innerhalb anderer zusammensetzbarer Funktionen verwendet werden. Wenn Sie eine Koroutine außerhalb einer zusammensetzbaren Funktion starten, aber so eingeschränkt sind, dass sie nach dem Verlassen der Zusammensetzung automatisch abgebrochen wird, verwenden Sie rememberCoroutineScope
.
Verwenden Sie rememberCoroutineScope
auch immer, 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 zusammensetzbare Funktion, die eine CoroutineScope
zurückgibt, die an den Punkt der Komposition gebunden ist, an dem sie aufgerufen wird. Der Bereich wird abgebrochen, wenn der Aufruf die Komposition verlässt.
Gemäß dem vorherigen Beispiel könnten Sie diesen Code verwenden, um ein Snackbar
anzuzeigen, 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
: auf einen Wert verweisen, der nicht neu gestartet werden sollte, wenn sich der Wert ändert
LaunchedEffect
wird neu gestartet, wenn sich einer der Schlüsselparameter ändert. In manchen Situationen möchten Sie jedoch möglicherweise einen Wert für Ihren Effekt erfassen, der besagt, dass er nicht neu gestartet werden soll, wenn er sich ändert. Dazu muss mit rememberUpdatedState
ein Verweis auf diesen Wert erstellt werden, der erfasst und aktualisiert werden kann. Dieser Ansatz ist hilfreich bei Effekten, die langlebige Vorgänge enthalten, deren Neuerstellung und Neustart teuer oder unmöglich sind.
Angenommen, Ihre Anwendung hat ein LandingScreen
, das nach einiger Zeit verschwindet. Auch wenn LandingScreen
neu zusammengesetzt wird, wartet der Effekt, dass einige Zeit wartet, und weist darauf hin, dass die verstrichene Zeit nicht neu gestartet werden sollte:
@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 erzeugen, der dem Lebenszyklus der Aufrufwebsite entspricht, wird eine sich nie ändernde Konstante wie Unit
oder true
als Parameter übergeben. Im Code oben wird LaunchedEffect(true)
verwendet. Damit das Lambda onTimeout
immer den letzten Wert enthält, mit dem LandingScreen
neu zusammengesetzt wurde, muss onTimeout
mit der Funktion rememberUpdatedState
zusammengefasst werden.
Das zurückgegebene State
, im Code currentOnTimeout
, sollte in diesem Effekt verwendet werden.
DisposableEffect
: Effekte, die bereinigt werden müssen
Verwenden Sie DisposableEffect
für Nebeneffekte, die nach dem Ändern der Schlüssel bereinigt werden müssen oder wenn die zusammensetzbare Funktion die Zusammensetzung verlässt.
Wenn sich die DisposableEffect
-Schlüssel ändern, muss die zusammensetzbare Funktion den aktuellen Effekt entsorgen (Bereinigen ausführen) und zurücksetzen, indem der Effekt noch einmal aufgerufen wird.
So können Sie beispielsweise Analyseereignisse basierend auf Lifecycle
-Ereignissen mit einem LifecycleObserver
senden.
Wenn Sie diese Ereignisse in Compose beobachten möchten, verwenden Sie DisposableEffect
, um den Beobachter bei Bedarf zu registrieren und seine 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 Code oben wird durch den Effekt das observer
zum lifecycleOwner
hinzugefügt. Wenn sich lifecycleOwner
ändert, wird der Effekt verworfen und mit dem neuen lifecycleOwner
neu gestartet.
Ein DisposableEffect
muss als endgültige Anweisung in seinem Codeblock eine onDispose
-Klausel enthalten. Andernfalls zeigt die IDE einen Build-Zeitfehler an.
SideEffect
: Erstellungsstatus in Nicht-Compose-Code veröffentlichen
Wenn Sie den Erstellungsstatus für Objekte freigeben möchten, die nicht von „Compose“ verwaltet werden, verwenden Sie die zusammensetzbare Funktion SideEffect
. Die Verwendung eines SideEffect
sorgt dafür, dass der Effekt nach jeder erfolgreichen Neuzusammensetzung wirksam wird. Andererseits ist es falsch, einen Effekt auszuführen, bevor eine erfolgreiche Neuzusammensetzung garantiert ist, was dann der Fall ist, wenn der Effekt direkt in eine zusammensetzbare Funktion geschrieben wird.
In Ihrer Analysebibliothek können Sie beispielsweise die Nutzerpopulation segmentieren, indem Sie allen nachfolgenden Analyseereignissen benutzerdefinierte Metadaten (in diesem Beispiel Nutzereigenschaften) hinzufügen. Wenn Sie den Nutzertyp des aktuellen Nutzers an Ihre Analysebibliothek kommunizieren möchten, verwenden Sie SideEffect
, um seinen 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-Zustand in Erstellungsstatus konvertieren
produceState
startet eine Koroutine, die auf die Komposition beschränkt ist und Werte in eine zurückgegebene State
übertragen kann. Sie können damit den Nicht-Compose-Status in den Erstellungsstatus konvertieren. So können Sie beispielsweise einen externen abobasierten Status wie Flow
, LiveData
oder RxJava
in die Zusammensetzung aufnehmen.
Der Producer wird gestartet, wenn produceState
die Komposition betritt, und abgebrochen, wenn er die Komposition verlässt. Der zurückgegebene State
wird zusammengefügt. Wenn Sie denselben Wert festlegen, wird keine Neuzusammensetzung ausgelöst.
Obwohl produceState
eine Koroutine erstellt, kann sie auch verwendet werden, um nicht anhaltende Datenquellen zu beobachten. Mit der Funktion awaitDispose
können Sie das Abo für diese Quelle entfernen.
Das folgende Beispiel zeigt, wie mit produceState
ein Image aus dem Netzwerk geladen wird. 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
Bei der Erstellung 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 neu zusammensetzen müssen. Das ist häufig der Fall, wenn sich etwas häufig ändert, z. B. eine Scrollposition, die zusammensetzbare Funktion aber erst dann reagieren muss, wenn sie einen bestimmten Schwellenwert überschreitet. derivedStateOf
erstellt ein neues Objekt für den Status „Compose“. Sie können sehen, dass es nur so oft aktualisiert wird, wie Sie benötigen. Auf diese Weise verhält er sich ähnlich wie der distinctUntilChanged()
-Operator für Kotlin-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 wird firstVisibleItemIndex
jedes Mal geändert, wenn sich das erste sichtbare Element ändert. Beim Scrollen ändert sich der Wert zu 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
Häufig wird fälschlicherweise angenommen, dass Sie beim Kombinieren von zwei Statusobjekten für die Zusammensetzung derivedStateOf
verwenden sollten, weil Sie den Status „ableiten“. Dies ist jedoch reiner Aufwand und nicht erforderlich, wie im folgenden Snippet gezeigt wird:
// 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 erfolgt keine übermäßige Neuzusammensetzung und die Verwendung von derivedStateOf
ist nicht erforderlich.
snapshotFlow
: Status von „Compose“ in Abläufe konvertieren
Verwenden Sie snapshotFlow
, um State<T>
-Objekte in einen kalten Ablauf zu konvertieren. snapshotFlow
führt beim Erfassen seinen Block aus 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 seinen Collector aus, wenn der neue Wert nicht dem vorherigen ausgegebenen Wert entspricht. Dieses Verhalten ähnelt dem von Flow.distinctUntilChanged
.
Das folgende Beispiel zeigt einen Nebeneffekt, bei dem erfasst wird, wenn der Nutzer nach dem ersten Element in einer Liste zu Analytics scrollt:
val listState = rememberLazyListState()
LazyColumn(state = listState) {
// ...
}
LaunchedEffect(listState) {
snapshotFlow { listState.firstVisibleItemIndex }
.map { index -> index > 0 }
.distinctUntilChanged()
.filter { it == true }
.collect {
MyAnalyticsService.sendScrolledPastFirstItemEvent()
}
}
Im Code oben wird listState.firstVisibleItemIndex
in einen Flow konvertiert, der von der Leistung der Flow-Operatoren profitieren kann.
Effekte neu starten
Für einige Effekte in der Funktion „Compose“, z. B. LaunchedEffect
, produceState
oder DisposableEffect
, ist eine variable Anzahl von Argumenten bzw. Schlüsseln erforderlich, mit denen der laufende Effekt abgebrochen und ein neues mit den neuen Schlüsseln 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 zum Neustart des Effekts verwendeten Parameter nicht die richtigen Parameter sind:
- Niedrigere Effekte für einen Neustart können zu Fehlern in Ihrer App führen.
- Ein stärkerer Neustart könnte ineffizient sein.
Als Faustregel sollten änderbare und unveränderliche Variablen, die im Effektblock des Codes verwendet werden, als Parameter für die zusammensetzbare Funktion hinzugefügt werden. Abgesehen von diesen können weitere Parameter hinzugefügt werden, um einen Neustart des Effekts zu erzwingen. Wenn die Änderung einer Variablen nicht zu einem Neustart führt, 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 übergeben.
Im oben gezeigten DisposableEffect
-Code wird der Effekt als Parameter des lifecycleOwner
in seinem Block verwendet, da jede Änderung an den Werten einen Neustart des Effekts auslösen 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
werden nicht als DisposableEffect
-Schlüssel benötigt, da sich ihr Wert in der Zusammensetzung aufgrund der Verwendung von rememberUpdatedState
nie ändert. Wenn Sie lifecycleOwner
nicht als Parameter übergeben und er sich ändert, wird HomeScreen
neu zusammengesetzt, DisposableEffect
aber nicht verworfen und neu gestartet. Das verursacht Probleme, 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 Anrufwebsite entspricht. Es gibt zulässige Anwendungsfälle dafür, wie das oben gezeigte LaunchedEffect
-Beispiel. Zuvor sollten Sie jedoch noch einmal
überlegen, ob das ist, was Sie brauchen.
Empfehlungen für dich
- Hinweis: Der Linktext wird angezeigt, wenn JavaScript deaktiviert ist.
- State und Jetpack Compose
- Kotlin für Jetpack Compose
- Ansichten in Compose verwenden