Ein Nebeneffekt ist eine Statusänderung der App, die außerhalb des Bereichs einer zusammensetzbaren 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 Nebeneffekte erforderlich, z. B. um ein einmaliges Ereignis wie das Anzeigen einer Snackbar oder das Aufrufen eines anderen Bildschirms bei einer bestimmten Statusbedingung auszulösen. Diese Aktionen sollten von einer kontrollierten Umgebung aus aufgerufen werden, die den Lebenszyklus der zusammensetzbaren Funktion kennt. Auf dieser Seite erfahren Sie mehr über die verschiedenen Nebeneffekt-APIs, die Jetpack Compose bietet.
Zustand und Wirkung – Anwendungsfälle
Wie in der Dokumentation Thinking in Compose beschrieben, sollten zusammensetzbare Funktionen 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.
Da sich in „Compose“ viele verschiedene Möglichkeiten bieten, können Effekte zu viel verwendet werden. Achten Sie darauf, dass Ihre Arbeit sich auf die Benutzeroberfläche bezieht und den unidirektionalen Datenfluss nicht unterbricht, wie in der Dokumentation zur Statusverwaltung erläutert.
LaunchedEffect
: Beendigungsfunktionen im Bereich einer zusammensetzbaren Funktion ausführen
Mit der zusammensetzbaren Funktion LaunchedEffect
können Sie Beendigungsfunktionen sicher innerhalb einer zusammensetzbaren Funktion aufrufen. Wenn LaunchedEffect
in die Zusammensetzung eingeht, wird eine Koroutine mit dem als Parameter übergebenen Codeblock gestartet. Die Koroutine wird abgebrochen, wenn LaunchedEffect
die Zusammensetzung verlässt. Wenn LaunchedEffect
mit anderen Schlüsseln neu zusammengesetzt wird (siehe Abschnitt Effekte neu starten unten), wird die vorhandene Koroutine abgebrochen und die neue Unterbrechungsfunktion in einer neuen Koroutine gestartet.
Zum Beispiel wird ein Snackbar
in einer Scaffold
-Funktion mit der SnackbarHostState.showSnackbar
-Funktion angezeigt, 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 obigen Code 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, wenn die Anweisung „false“ ist und LaunchedEffect
in der Komposition enthalten war, diese entfernt. Daher wird die Koroutine abgebrochen.
rememberCoroutineScope
: Erlangt einen kompositionsbezogenen Bereich, um eine Koroutine außerhalb einer zusammensetzbaren Funktion zu starten.
Da LaunchedEffect
eine zusammensetzbare Funktion ist, kann sie nur in anderen zusammensetzbaren Funktionen verwendet werden. Wenn Sie eine Koroutine außerhalb einer zusammensetzbaren Funktion starten möchten, aber auf einen bestimmten Bereich beschränkt ist, damit sie automatisch abgebrochen wird, sobald sie die Zusammensetzung 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 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.
Entsprechend dem vorherigen Beispiel kannst du diesen Code verwenden, um ein Snackbar
anzuzeigen, 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
: Verweist auf einen Wert in einem Effekt, der nicht neu gestartet werden sollte, wenn sich der Wert ändert
LaunchedEffect
wird neu gestartet, wenn sich einer der Schlüsselparameter ändert. In einigen Situationen kann es jedoch sinnvoll sein, einen Wert für den Effekt zu erfassen, der bei einer Änderung nicht neu gestartet werden soll. 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 Anwendung hat ein LandingScreen
, das nach einiger Zeit verschwindet. Selbst wenn LandingScreen
neu zusammengesetzt wird, wird der Effekt, der eine Weile wartet und benachrichtigt, dass die vergangene 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 ständig ändernde Konstante wie Unit
oder true
als Parameter übergeben. Im Code oben wird LaunchedEffect(true)
verwendet. Damit onTimeout
Lambda immer den letzten Wert enthält, mit dem LandingScreen
neu zusammengesetzt wurde, muss onTimeout
mit der rememberUpdatedState
-Funktion zusammengefasst werden.
Der zurückgegebene State
(currentOnTimeout
im Code) sollte dabei verwendet werden.
DisposableEffect
: Effekte, die bereinigt werden müssen
Für Nebeneffekte, die nach einer Änderung der Schlüssel oder wenn die zusammensetzbare Funktion die Komposition verlässt, bereinigt werden müssen, verwende DisposableEffect
.
Wenn sich die DisposableEffect
-Schlüssel ändern, muss die zusammensetzbare Funktion ihren aktuellen Effekt beseitigen, d. h. bereinigen, und zurückgesetzt werden, indem der Effekt noch einmal aufgerufen wird.
Beispielsweise können Sie mithilfe von LifecycleObserver
Analyseereignisse senden, die auf Lifecycle
-Ereignissen basieren.
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 observer
zum lifecycleOwner
hinzugefügt. Wenn sich lifecycleOwner
ändert, wird der Effekt entfernt und mit der neuen lifecycleOwner
neu gestartet.
Eine DisposableEffect
muss eine onDispose
-Klausel als letzte Anweisung in ihrem Codeblock 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 durch die Zusammensetzung verwaltet werden, verwenden Sie die zusammensetzbare Funktion SideEffect
. Die Verwendung einer SideEffect
garantiert, dass der Effekt nach jeder erfolgreichen Neuzusammensetzung wirksam wird. Andererseits ist es falsch, einen Effekt auszuführen, bevor eine erfolgreiche Neuzusammensetzung garantiert ist. Dies ist der Fall, wenn der Effekt direkt in eine zusammensetzbare Funktion geschrieben wird.
Mithilfe der Analysebibliothek können Sie beispielsweise die Nutzerpopulation segmentieren, indem Sie allen nachfolgenden Analyseereignissen benutzerdefinierte Metadaten (in diesem Beispiel „Nutzereigenschaften“) hinzufügen. Verwenden Sie SideEffect
, um den Wert des aktuellen Nutzers zu aktualisieren und der Analysebibliothek den Nutzertyp des aktuellen Nutzers mitzuteilen.
@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 Koroutine für die Zusammensetzung, die Werte in ein zurückgegebenes State
übertragen kann. Damit können Sie den Status ohne Erstellung in den Erstellungsstatus umwandeln, z. B. externe, abogesteuerte Status wie Flow
, LiveData
oder RxJava
in die Komposition.
Der Producer wird gestartet, wenn produceState
die Komposition betritt, und wird abgebrochen, wenn er die Komposition verlässt. Der zurückgegebene State
führt zu einer Zusammenführung. 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. Mit der Funktion awaitDispose
können Sie das Abo für diese Quelle entfernen.
Das folgende Beispiel zeigt, wie Sie mit produceState
ein Image 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
: Konvertiert ein oder mehrere Zustandsobjekte in einen anderen Status
Bei der Zusammensetzung erfolgt jedes Mal eine Neuzusammensetzung, 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 Ihre 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. 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. Wenn Sie 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
Ein häufiger Fehler besteht darin, anzunehmen, dass Sie beim Kombinieren von zwei Compose-Zustandsobjekten derivedStateOf
verwenden sollten, da Sie einen "Ableiten des Zustands" vornehmen. Wie im folgenden Snippet gezeigt, ist dies jedoch reiner Aufwand und nicht erforderlich:
// 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
: 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 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
).
Das folgende Beispiel zeigt einen Nebeneffekt, der erfasst, wenn der Nutzer in Analytics über das erste Element 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 konvertiert, der von der Leistungsfähigkeit der Flow-Operatoren profitieren kann.
Effekte neu starten
Einige Effekte in Compose, z. B. LaunchedEffect
, produceState
oder DisposableEffect
, erfordern eine variable Anzahl von Argumenten oder Schlüsseln, mit denen der laufende Effekt abgebrochen und ein neuer mit den neuen Schlüsseln gestartet wird.
Die typische Form für diese APIs lautet:
EffectName(restartIfThisKeyChanges, orThisKey, orThisKey, ...) { block }
Aufgrund der Feinheiten dieses Verhaltens können Probleme auftreten, wenn die Parameter, die zum Neustarten des Effekts verwendet werden, nicht die richtigen sind:
- Wenn ein Neustart weniger Auswirkungen hat, als er sollte, können Fehler in der App auftreten.
- Ein Neustart kann ineffizient sein, als sie eigentlich sein sollten.
Als Faustregel gilt, dass änderbare und unveränderliche Variablen, die im Effektblock des Codes verwendet werden, dem zusammensetzbaren Effekt als Parameter hinzugefügt werden sollten. Darüber hinaus können weitere Parameter hinzugefügt werden, um einen Neustart des Effekts 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
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 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 Auswirkungsschlüssel verwenden, damit sie dem Lebenszyklus der Aufrufwebsite folgt. Dafür gibt es gültige Anwendungsfälle wie das LaunchedEffect
-Beispiel oben. Ü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.
- State und Jetpack Compose
- Kotlin für Jetpack Compose
- Ansichten in „Compose“ verwenden