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 composabili, come ricostruzioni imprevedibili, esecuzione di ricostruzioni di composabili in ordini diversi o ricostruzioni che possono essere eliminate, idealmente i composabili dovrebbero essere privi di effetti collaterali.
Tuttavia, a volte sono necessari effetti collaterali, ad esempio per attivare un singolo evento come la visualizzazione di una barra di notifica o il passaggio a un'altra schermata in base a una determinata condizione di stato. Queste azioni devono essere chiamate da un ambiente controllato che sia a conoscenza del ciclo di vita del composable. In questa pagina scoprirai le diverse API con effetti collaterali offerte da Jetpack Compose.
Casi d'uso relativi a stato ed effetto
Come descritto nella documentazione Pensare in Compose, i composabili non devono avere effetti collaterali. Quando devi apportare modifiche allo stato dell'app (come descritto nella documentazione Gestione dello stato), devi utilizzare le API Effect in modo che gli effetti collaterali vengano eseguiti in modo prevedibile.
A causa delle diverse possibilità offerte dagli effetti in Scrittura, possono essere facilmente sovrautilizzati. Assicurati che il lavoro che svolgi in queste app 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 composable
Per eseguire operazioni durante la vita di un componibile e avere la possibilità di chiamare funzioni di sospensione, utilizza il 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 riformata con chiavi diverse (vedi la sezione Effetti del riavvio di seguito), la coroutine esistente verrà annullata e la nuova funzione di sospensione verrà lanciata in una nuova coroutine.
Ad esempio, di seguito è riportata un'animazione che emette impulsi al valore alpha 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 riportato sopra, l'animazione utilizza la funzione di sospensione
delay
per attendere il tempo impostato. Quindi, anima in sequenza l'alpha
da zero e viceversa utilizzando
animateTo
.
L'operazione verrà ripetuta per tutta la durata del composable.
rememberCoroutineScope
: ottieni un ambito sensibile alla composizione per avviare una coroutine all'esterno di un componente composable
Poiché LaunchedEffect
è una funzione componibile, può essere utilizzata solo all'interno di altre funzioni comibili. Per avviare una coroutine al di fuori di un composable, ma con ambito in modo che venga annullata automaticamente quando esce dalla composizione, utilizza rememberCoroutineScope
.
Utilizza rememberCoroutineScope
anche ogni volta che devi controllare manualmente il ciclo di vita di una o più coroutine, ad esempio annullare un'animazione quando si verifica un evento utente.
rememberCoroutineScope
è una funzione componibile che restituisce un
CoroutineScope
vincolato al punto della composizione in cui viene chiamata. L'ambito verrà annullato quando la chiamata esce dalla composizione.
Seguendo l'esempio precedente, puoi 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
: fai riferimento a un valore in un effetto che non deve essere riavviato se il valore cambia
LaunchedEffect
si riavvia 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 un po' di tempo
e comunica 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 passata come parametro una costante immutabile come Unit
o true
. Nel codice riportato sopra viene utilizzato LaunchedEffect(true)
. Per assicurarti che la funzione lambda onTimeout
sempre contenga l'ultimo valore con cui è stato ricomposto LandingScreen
, onTimeout
deve essere racchiusa nella funzione rememberUpdatedState
.
I valori State
, currentOnTimeout
restituiti nel codice devono essere utilizzati nell'effetto.
DisposableEffect
: effetti che richiedono la pulizia
Per gli effetti collaterali che devono essere ripuliti dopo la modifica delle chiavi o se il composable esce dalla composizione, utilizza DisposableEffect
.
Se le chiavi DisposableEffect
cambiano, il composable deve rimuovere (eseguire la pulizia di) l'effetto corrente e reimpostarlo chiamando di nuovo l'effetto.
Ad esempio, potresti voler inviare eventi di analisi basati su
eventi Lifecycle
utilizzando un
LifecycleObserver
.
Per ascoltare questi eventi in Scrittura, utilizza un DisposableEffect
per registrare e annullare la registrazione dell'osservatore, se 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 riportato sopra, 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 istruzione finale nel suo blocco di codice. In caso contrario, l'IDE mostra un errore di compilazione.
SideEffect
: pubblica lo stato di composizione nel codice non di composizione
Per condividere lo stato di composizione con oggetti non gestiti da compose, utilizza il composable
SideEffect
. L'utilizzo di un SideEffect
garantisce che l'effetto venga eseguito dopo ogni
ricomposizione riuscita. D'altra parte, non è corretto eseguire un effetto prima di garantire una ricompozione riuscita, come accade quando si scrive l'effetto direttamente in un composable.
Ad esempio, la tua libreria di analisi potrebbe consentirti di segmentare la popolazione di utenti collegando metadati personalizzati ("proprietà utente" in questo esempio) a tutti gli eventi di analisi successivi. Per comunicare il tipo di utente
dell'utente corrente alla tua 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 limitata alla composizione che può inviare valori a un
State
restituito. Utilizzalo per
convertire lo stato non Compose in stato Compose, ad esempio inserendo nello stato
Composizione lo stato esterno basato sugli abbonamenti come Flow
, LiveData
o RxJava
.
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 ricostituzione.
Anche se produceState
crea una coroutine, può essere utilizzato anche per osservare
le origini di dati che non vengono sospese. Per rimuovere l'abbonamento a questa origine, utilizza la funzione
awaitDispose
.
L'esempio seguente mostra come utilizzare produceState
per caricare un'immagine dalla rete. La funzione composable loadNetworkImage
restituisce un State
che può essere utilizzato in altri composable.
@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 ricompozione avviene ogni volta che un oggetto dello stato osservato o un input composable cambia. Un oggetto stato o un input potrebbe cambiare più spesso di quanto l'interfaccia utente debba effettivamente aggiornarsi, con conseguente ricompozione non necessaria.
Devi utilizzare la funzione derivedStateOf
quando gli input di un composable cambiano più spesso di quanto sia necessario per eseguire la ricompozione. Questo accade spesso quando qualcosa cambia di frequente, ad esempio una posizione di scorrimento, ma il composable deve reagire solo quando supera una determinata soglia. derivedStateOf
crea un nuovo oggetto dello stato Compose che puoi osservare e che si aggiorna solo in base alle tue esigenze. In questo modo, agisce 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 ricompozione deve avvenire solo se il valore è maggiore di 0
.
Questa mancata corrispondenza nella frequenza di aggiornamento indica che si tratta di un buon caso d'uso per
derivedStateOf
.
Utilizzo non corretto
Un errore comune è assumere che, quando combini due oggetti dello stato di Compose,
dovresti utilizzare derivedStateOf
perché stai "derivando lo stato". Tuttavia, questo
è puramente un overhead e non è necessario, come mostrato nello snippet seguente:
// 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 ricostituzione 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 a freddo. snapshotFlow
esegue il proprio blocco quando viene raccolto ed emette il risultato degli oggetti State
letti al suo interno. Quando uno degli oggetti State
letti all'interno del blocco snapshotFlow
subisce una mutazione, il flusso emette il nuovo valore
al suo collettore se il nuovo valore non è uguale a
il valore emesso in precedenza (questo comportamento è simile a quello di
Flow.distinctUntilChanged
).
Il seguente esempio mostra un effetto collaterale che registra quando l'utente scorri 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 riportato sopra, listState.firstVisibleItemIndex
viene convertito in un flusso che può sfruttare la potenza degli operatori di Flow.
Riavvio degli effetti
Alcuni effetti in Componi, 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.
La forma tipica di 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:
- Se riavvii gli effetti meno di quanto dovresti, potresti causare bug nella tua app.
- Riavviare gli effetti più di quanto dovrebbe essere necessario potrebbe non essere efficiente.
Come regola generale, le variabili mutabili e immutabili utilizzate nel blocco dell'effetto del codice devono essere aggiunte come parametri al composable dell'effetto. Oltre a questi,
si possono 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 tra rememberUpdatedState
. Se la variabile non cambia mai perché è racchiusa in un remember
senza chiavi, non è necessario passarla come chiave all'effetto.
Nel codice DisposableEffect
mostrato sopra, l'effetto prende come parametro il
lifecycleOwner
utilizzato nel relativo blocco, perché qualsiasi modifica dovrebbe 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
si ricomporrà, ma DisposableEffect
non verrà eliminato e riavviato. Ciò causa problemi perché da quel momento in poi viene utilizzato 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 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 Scrittura