Un effetto collaterale è una modifica dello stato dell'app che si verifica al di fuori dell'ambito di una funzione componibile. A causa del ciclo di vita e delle proprietà dei composable, come le ricomposizioni imprevedibili, l'esecuzione di ricomposizioni di composable in ordini diversi o le ricomposizioni che possono essere eliminate, i composable devono idealmente essere privi di effetti collaterali.
Tuttavia, a volte gli effetti collaterali sono necessari, ad esempio per attivare un evento una tantum come la visualizzazione di una snackbar o per passare a un'altra schermata in base a una determinata condizione di stato. Queste azioni devono essere chiamate da un ambiente controllato che conosca il ciclo di vita del componente componibile. In questa pagina, scoprirai le diverse API per gli effetti collaterali offerte da Jetpack Compose.
Casi d'uso di stato ed effetto
Come descritto nella documentazione Thinking in Compose, i composable non devono avere effetti collaterali. Quando devi apportare modifiche allo stato dell'app (come descritto nel documento Gestione dello stato), devi utilizzare le API Effect in modo che questi effetti collaterali vengano eseguiti in modo prevedibile.
A causa delle diverse possibilità che gli effetti aprono in Scrittura, possono essere facilmente usati in modo eccessivo. Assicurati che il lavoro che svolgi sia correlato all'interfaccia utente e non interrompa il flusso di dati unidirezionale, come spiegato nella documentazione sulla gestione dello stato.
LaunchedEffect
: esegui funzioni di sospensione nell'ambito di un'attività componibile
Per eseguire operazioni durante il ciclo di vita di un elemento componibile e avere la possibilità di chiamare
funzioni di sospensione, utilizza l'elemento componibile
LaunchedEffect
. Quando LaunchedEffect
entra nella composizione, avvia una
coroutine con il blocco di codice passato come parametro. La coroutine verrà
annullata se LaunchedEffect
esce dalla composizione. Se LaunchedEffect
viene
ricomposto con chiavi diverse (vedi la sezione Riavvio
degli effetti di seguito), la coroutine esistente verrà
annullata e la nuova funzione di sospensione verrà avviata in una nuova coroutine.
Ad esempio, ecco un'animazione che pulsa il valore alfa con un ritardo configurabile:
// 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) } }
Nel codice precedente, l'animazione utilizza la funzione di sospensione
delay
per attendere il periodo di tempo impostato. Poi, anima in sequenza l'alpha
a zero e di nuovo indietro usando
animateTo
.
Questa operazione verrà ripetuta per l'intero ciclo di vita del componente componibile.
rememberCoroutineScope
: ottenere un ambito sensibile alla composizione per avviare una coroutine al di fuori di un elemento componibile
Poiché LaunchedEffect
è una funzione componibile, può essere utilizzata solo all'interno di altre
funzioni componibili. Per avviare una coroutine al di fuori di un elemento componibile, ma con ambito in modo che venga annullata automaticamente una volta uscita dalla composizione, utilizza rememberCoroutineScope
.
Utilizza rememberCoroutineScope
anche quando devi controllare manualmente il ciclo di vita di una o più coroutine, ad esempio annullando un'animazione quando si verifica un evento utente.
rememberCoroutineScope
è una funzione componibile che restituisce un
CoroutineScope
associato al punto della composizione in cui viene chiamato. L'ambito
verrà annullato quando la chiamata esce dalla composizione.
Seguendo l'esempio precedente, potresti utilizzare questo codice per mostrare un Snackbar
quando l'utente tocca un Button
:
@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
: fa riferimento a un valore in un effetto che non deve essere riavviato se il valore cambia
LaunchedEffect
viene riavviato quando uno dei parametri chiave cambia. Tuttavia, in
alcune situazioni potresti voler acquisire un valore nell'effetto che, se
cambia, non vuoi che l'effetto venga riavviato. Per farlo, è
necessario utilizzare rememberUpdatedState
per creare un riferimento a questo valore che
può essere acquisito e aggiornato. Questo approccio è utile per gli effetti che contengono
operazioni di lunga durata che potrebbero essere costose o proibitive da ricreare e
riavviare.
Ad esempio, supponiamo che la tua app abbia un LandingScreen
che scompare dopo un po' di tempo. Anche se LandingScreen
viene ricomposto, l'effetto che attende per un po' di tempo
e notifica che il tempo è trascorso non deve essere riavviato:
@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 */ }
Per creare un effetto che corrisponda al ciclo di vita del sito di chiamata, viene trasmessa come parametro una costante che non cambia mai, ad esempio Unit
o true
. Nel codice
sopra, viene utilizzato LaunchedEffect(true)
. Per assicurarti che la lambda onTimeout
contenga sempre l'ultimo valore con cui è stato ricomposto LandingScreen
, onTimeout
deve essere racchiuso nella funzione rememberUpdatedState
.
State
e currentOnTimeout
restituiti nel codice devono essere utilizzati nell'effetto.
DisposableEffect
: effetti che richiedono pulizia
Per gli effetti collaterali che devono essere puliti dopo la modifica delle chiavi o se il
componente componibile esce dalla composizione, utilizza
DisposableEffect
.
Se le chiavi DisposableEffect
cambiano, il composable deve eliminare (eseguire
la pulizia per) il suo effetto attuale e reimpostarlo chiamando di nuovo l'effetto.
Ad esempio, potresti voler inviare eventi Analytics basati su
eventi Lifecycle
utilizzando un
LifecycleObserver
.
Per rilevare questi eventi in Compose, utilizza un DisposableEffect
per registrare e
annullare la registrazione dell'observer quando necessario.
@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 */ }
Nel codice precedente, l'effetto aggiungerà observer
a
lifecycleOwner
. Se lifecycleOwner
cambia, l'effetto viene eliminato e
riavviato con il nuovo lifecycleOwner
.
Un DisposableEffect
deve includere una clausola onDispose
come ultima istruzione
nel blocco di codice. In caso contrario, l'IDE visualizza un errore in fase di compilazione.
SideEffect
: pubblica lo stato di composizione nel codice non Compose
Per condividere lo stato di composizione con oggetti non gestiti dalla composizione, utilizza il composable
SideEffect
. L'utilizzo di un SideEffect
garantisce che l'effetto venga eseguito dopo ogni ricomposizione riuscita. D'altra parte, è errato
eseguire un effetto prima che sia garantita una ricomposizione riuscita, il che si verifica quando l'effetto viene scritto direttamente in un elemento componibile.
Ad esempio, la tua libreria di analisi potrebbe consentirti di segmentare la popolazione di utenti
allegando metadati personalizzati ("proprietà utente" in questo esempio)
a tutti gli eventi di analisi successivi. Per comunicare il tipo di utente
dell'utente attuale alla libreria di analisi, utilizza SideEffect
per aggiornarne il valore.
@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
: converti lo stato non di composizione in stato di composizione
produceState
avvia una coroutine con ambito nella composizione che può inserire valori in un
State
restituito. Utilizzalo per convertire lo stato non di composizione in stato di composizione, ad esempio per portare lo stato esterno basato su abbonamento, come Flow
, LiveData
o RxJava
, nella composizione.
Il produttore viene avviato quando produceState
entra nella composizione e viene
annullato quando esce dalla composizione. Il valore State
restituito viene unito;
l'impostazione dello stesso valore non attiverà una ricomposizione.
Anche se produceState
crea una coroutine, può essere utilizzato anche per osservare
origini dati non sospese. Per rimuovere l'iscrizione a questa origine, utilizza la funzione
awaitDispose
.
L'esempio seguente mostra come utilizzare produceState
per caricare un'immagine dalla rete. La funzione componibile loadNetworkImage
restituisce un State
che può
essere utilizzato in altri componibili.
@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
: converti uno o più oggetti stato in un altro stato
In Compose, la ricomposizione si verifica ogni volta che cambia un oggetto di stato osservato o un input componibile. Un oggetto stato o un input potrebbe cambiare più spesso di quanto l'interfaccia utente debba effettivamente aggiornarsi, causando una ricomposizione non necessaria.
Devi utilizzare la funzione derivedStateOf
quando gli input di un elemento componibile cambiano più spesso di quanto sia necessario
ricomporre. Ciò si verifica spesso quando qualcosa cambia frequentemente, ad esempio
una posizione di scorrimento, ma il composable deve reagire solo quando supera
una determinata soglia. derivedStateOf
crea un nuovo oggetto di stato Compose che puoi osservare e che si aggiorna solo in base alle tue esigenze. In questo modo, si comporta in modo simile all'operatore distinctUntilChanged()
di Kotlin Flows.
Utilizzo corretto
Il seguente snippet mostra un caso d'uso appropriato per 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 questo snippet, firstVisibleItemIndex
cambia ogni volta che cambia il primo elemento visibile. Man mano che scorri, il valore diventa 0
, 1
, 2
, 3
, 4
, 5
e così via.
Tuttavia, la ricomposizione deve avvenire solo se il valore è maggiore di 0
.
Questa mancata corrispondenza nella frequenza di aggiornamento significa che questo è un buon caso d'uso per
derivedStateOf
.
Utilizzo non corretto
Un errore comune è supporre che, quando combini due oggetti di stato Compose,
devi utilizzare derivedStateOf
perché stai "derivando lo stato". Tuttavia, questo
è puramente overhead e non è necessario, come mostrato nel seguente snippet:
// 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 questo snippet, fullName
deve essere aggiornato con la stessa frequenza di firstName
e
lastName
. Pertanto, non si verifica alcuna ricomposizione in eccesso e l'utilizzo di
derivedStateOf
non è necessario.
snapshotFlow
: converti lo stato di Compose in flussi
Utilizza snapshotFlow
per convertire gli oggetti State<T>
in un flusso freddo. snapshotFlow
esegue il blocco quando viene raccolto ed emette
il risultato degli oggetti State
letti. Quando uno degli oggetti State
letti all'interno del blocco snapshotFlow
subisce una mutazione, il flusso emette il nuovo valore
nel suo raccoglitore se il nuovo valore non è uguale
al valore emesso in precedenza (questo comportamento è simile a quello di
Flow.distinctUntilChanged
).
Il seguente esempio mostra un effetto collaterale che registra quando l'utente scorre oltre il primo elemento di un elenco in Analytics:
val listState = rememberLazyListState()
LazyColumn(state = listState) {
// ...
}
LaunchedEffect(listState) {
snapshotFlow { listState.firstVisibleItemIndex }
.map { index -> index > 0 }
.distinctUntilChanged()
.filter { it == true }
.collect {
MyAnalyticsService.sendScrolledPastFirstItemEvent()
}
}
Nel codice precedente, listState.firstVisibleItemIndex
viene convertito in un flusso che
può sfruttare la potenza degli operatori di Flow.
Riavvio degli effetti
Alcuni effetti in Compose, come LaunchedEffect
, produceState
o
DisposableEffect
, accettano un numero variabile di argomenti, chiavi, che vengono utilizzati per
annullare l'effetto in esecuzione e avviarne uno nuovo con le nuove chiavi.
Il modulo tipico per queste API è:
EffectName(restartIfThisKeyChanges, orThisKey, orThisKey, ...) { block }
A causa delle sottigliezze di questo comportamento, possono verificarsi problemi se i parametri utilizzati per riavviare l'effetto non sono quelli giusti:
- Il riavvio degli effetti meno del dovuto potrebbe causare bug nell'app.
- Riavviare gli effetti più del dovuto potrebbe essere inefficiente.
Come regola generale, le variabili mutabili e immutabili utilizzate nel blocco di effetti del
codice devono essere aggiunte come parametri al composable dell'effetto. A parte questi,
è possibile aggiungere altri parametri per forzare il riavvio dell'effetto. Se la modifica di
una variabile non deve causare il riavvio dell'effetto, la variabile deve essere racchiusa
in rememberUpdatedState
. Se la variabile non cambia mai perché è racchiusa in un remember
senza chiavi, non devi passarla come chiave all'effetto.
Nel codice DisposableEffect
mostrato sopra, l'effetto prende come parametro il
lifecycleOwner
utilizzato nel suo blocco, perché qualsiasi modifica apportata deve causare il
riavvio dell'effetto.
@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
e currentOnStop
non sono necessari come chiavi DisposableEffect
, perché il loro valore non cambia mai nella composizione a causa dell'utilizzo di rememberUpdatedState
. Se non passi lifecycleOwner
come parametro e
questo cambia, HomeScreen
viene ricomposto, ma DisposableEffect
non viene eliminato
e riavviato. Ciò causa problemi perché da quel momento in poi viene utilizzato il lifecycleOwner
sbagliato.
Costanti come chiavi
Puoi utilizzare una costante come true
come chiave dell'effetto per
farla seguire il ciclo di vita del sito di chiamata. Esistono casi d'uso validi, come l'esempio LaunchedEffect
mostrato sopra. Tuttavia, prima di farlo,
rifletti bene e assicurati che sia quello che ti serve.
Consigliati per te
- Nota: il testo del link viene visualizzato quando JavaScript è disattivato
- State e Jetpack Compose
- Kotlin per Jetpack Compose
- Utilizzare le visualizzazioni in Scrivi