Un effetto collaterale è una modifica allo stato dell'app che si verifica al di fuori dell'ambito di una funzione componibile. A causa del ciclo di vita dei componibili e di proprietà come le ricomposizioni imprevedibili, l'esecuzione di ricomposizioni di componibili in ordini diversi o le ricomposizioni che possono essere ignorate, i componibili idealmente non dovrebbero avere 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 la navigazione verso 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 per gli effetti collaterali offerte da Jetpack Compose.
Casi d'uso di stato ed effetto
Come spiegato nella documentazione Thinking in Compose, i componibili non devono avere effetti collaterali. Quando devi apportare modifiche allo stato dell'app (come descritto nella documentazione sulla gestione dello stato), devi utilizzare le API Effect in modo che questi effetti collaterali vengano eseguiti in modo prevedibile.
A causa delle diverse possibilità offerte dagli effetti in Compose, è facile abusarne. Assicurati che il lavoro che svolgi al loro interno sia correlato alla UI e non interrompa il flusso di dati unidirezionale, come spiegato nella documentazione sulla gestione dello stato.
LaunchedEffect: esegui le funzioni di sospensione nell'ambito di un componibile
Per eseguire il lavoro per tutta la durata 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 ricomposto con chiavi diverse (consulta 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 fa pulsare 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 { mutableLongStateOf(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. Poi, anima in sequenza l'alpha
a zero e viceversa utilizzando
animateTo.
Questo processo si ripeterà per tutta la durata del composable.
rememberCoroutineScope: ottieni uno scope sensibile alla composizione per avviare una coroutine all'esterno di un composable
Poiché LaunchedEffect è una funzione componibile, può essere utilizzata solo all'interno di altre funzioni componibili. Per avviare una coroutine all'esterno di un composable, ma con un ambito tale da essere annullata automaticamente una volta uscita dalla composizione, utilizza rememberCoroutineScope.
Utilizza rememberCoroutineScope anche ogni volta che devi controllare manualmente il ciclo di vita di una o più coroutine, ad esempio per annullare 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, 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: fa 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 nel tuo 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 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 passata come parametro una costante che non cambia mai, come Unit o true. Nel codice riportato sopra, viene utilizzato LaunchedEffect(true). Per assicurarti che la funzione lambda onTimeout contenga sempre l'ultimo valore con cui è stato ricomposto LandingScreen, onTimeout deve essere aggregato con la funzione rememberUpdatedState.
Il valore State restituito, currentOnTimeout nel codice, deve essere utilizzato nell'effetto.
DisposableEffect: effetti che richiedono la pulizia
Per gli effetti collaterali che devono essere ripuliti dopo la modifica delle chiavi o se il componibile esce dalla composizione, utilizza DisposableEffect.
Se le chiavi DisposableEffect cambiano, il composable deve eliminare (eseguire la pulizia) l'effetto corrente e reimpostarlo chiamando nuovamente l'effetto.
Ad esempio, potresti voler inviare eventi di analisi in base agli
eventi Lifecycle
utilizzando un
LifecycleObserver.
Per ascoltare 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 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 visualizza un errore in fase di build.
SideEffect: pubblica lo stato di Compose nel codice non Compose
Per condividere lo stato di Compose con oggetti non gestiti da Compose, utilizza il componibile 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 che sia garantita una ricomposizione riuscita, come avviene quando l'effetto viene scritto direttamente in un componibile.
Ad esempio, la 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 corrente alla libreria di analisi, utilizza SideEffect per aggiornare il relativo 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 Compose in stato Compose
produceState
avvia una coroutine con ambito limitato alla composizione che può inserire valori in un
State restituito. Utilizzalo per convertire lo stato non Compose in stato Compose, ad esempio per inserire nella composizione uno stato esterno basato su abbonamento come Flow, LiveData o RxJava.
Il producer 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 quell'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: converte uno o più oggetti di stato in un altro stato
In Compose, la ricomposizione avviene ogni volta che un oggetto di stato osservato o un input componibile cambia. Un oggetto di stato o un input potrebbe cambiare più spesso di quanto l'UI debba effettivamente aggiornarsi, causando una ricomposizione non necessaria.
Devi utilizzare la funzione derivedStateOf quando gli input di un composable cambiano più spesso di quanto sia necessario per la ricomposizione. Questo accade spesso quando un elemento 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 di stato di Compose che puoi osservare e che si aggiorna solo quando necessario. In questo modo, agisce in modo simile all'operatore distinctUntilChanged() di Kotlin Flow.
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 è presumere che, quando combini due oggetti di stato di Compose,
devi utilizzare derivedStateOf perché stai "derivando lo stato". Tuttavia, questo è un overhead puro e non è obbligatorio, 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 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 Flow freddo. snapshotFlow esegue il suo 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 cambia, il flusso emette il nuovo valore al relativo raccoglitore se il nuovo valore non è uguale al valore emesso in precedenza (questo comportamento è simile a quello di Flow.distinctUntilChanged).
L'esempio seguente 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 riportato sopra, listState.firstVisibleItemIndex viene convertito in un Flow 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, ovvero 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 corretti:
- Il riavvio degli effetti meno spesso di quanto dovrebbe potrebbe causare bug nell'app.
- Il riavvio di un numero di effetti superiore a quello necessario potrebbe essere inefficiente.
Come regola generale, le variabili modificabili e non modificabili utilizzate nel blocco di codice dell'effetto devono essere aggiunte come parametri al composable dell'effetto. Oltre a 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 inserita in rememberUpdatedState. Se la variabile non cambia mai perché è inserita in un remember senza chiavi, non è necessario passare la variabile come chiave all'effetto.
Nel codice DisposableEffect mostrato sopra, l'effetto accetta come parametro il valore lifecycleOwner utilizzato nel suo 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é i loro valori non cambiano mai in Composition 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. Questo causa problemi perché da quel momento in poi viene utilizzato il lifecycleOwner sbagliato.
Costanti come chiavi
Puoi utilizzare una costante come true come chiave di effetto per fare in modo che segua il ciclo di vita del sito di chiamata. Esistono casi d'uso validi per questo attributo, come l'esempio di LaunchedEffect mostrato sopra. Tuttavia, prima di farlo,
rifletti bene e assicurati che sia ciò di cui hai bisogno.
Consigliati per te
- Nota: il testo del link viene visualizzato quando JavaScript è disattivato
- Stato e Jetpack Compose
- Kotlin per Jetpack Compose
- Utilizzo delle visualizzazioni in Compose