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 componibili, come le ricomposizioni imprevedibili, l'esecuzione delle ricomposizioni dei componibili in ordini diversi o le ricomposizioni che possono essere eliminate, i componibili dovrebbero 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 la navigazione a un'altra schermata in base a una determinata condizione di stato. Queste azioni devono essere chiamate da un ambiente controllato che riconosce il ciclo di vita del componibile. In questa pagina scoprirai le diverse API per gli effetti collaterali offerte da Jetpack Compose.
Casi d'uso di stato ed effetti
Come indicato nella Pensare in Compose documentazione, i componibili devono essere privi di effetti collaterali. Quando devi apportare modifiche allo stato dell'app (come descritto nel documento della documentazione Gestire lo stato), devi utilizzare le API per gli effetti in modo che questi effetti collaterali vengano eseguiti in modo prevedibile.
A causa delle diverse possibilità che gli effetti aprono in Compose, possono essere facilmente utilizzati in modo eccessivo. Assicurati che il lavoro che svolgi sia correlato all'UI e non interrompa il flusso di dati unidirezionale come spiegato nella documentazione Gestire lo stato.
LaunchedEffect: esegui le funzioni di sospensione nell'ambito di un componibile
Per eseguire il lavoro durante la durata di un componibile e avere la possibilità di chiamare
funzioni di sospensione, utilizza il
LaunchedEffect
componibile. 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 Riavviare
gli 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 { 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 precedente, l'animazione utilizza la funzione di sospensione
delay
per attendere il periodo di tempo impostato. Poi, anima sequenzialmente l'alfa
a zero e di nuovo utilizzando
animateTo.
Questa operazione verrà ripetuta per la durata del componibile.
rememberCoroutineScope: ottieni un ambito sensibile alla composizione per avviare una coroutine al di fuori di un 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 componibile,
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 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 chiamata. L'ambito verrà annullato quando la chiamata esce dalla composizione.
Seguendo l'esempio precedente, potresti utilizzare questo codice per mostrare una 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 viene riavviato quando uno dei parametri della 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 una 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 precedente 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.
Lo State restituito, currentOnTimeout nel codice, deve essere utilizzato nell'effetto.
DisposableEffect: effetti che richiedono la pulizia
Per gli effetti collaterali che devono essere puliti dopo la modifica delle chiavi o se il
componibile esce dalla composizione, utilizza
DisposableEffect.
Se le chiavi DisposableEffect cambiano, il componibile deve eliminare (eseguire la pulizia per) l'effetto corrente e reimpostare chiamando di nuovo l'effetto.
Ad esempio, potresti voler inviare eventi di analisi in base agli eventi
Lifecycleutilizzando 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 precedente, l'effetto aggiungerà l'observer al 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 Compose nel codice non Compose
Per condividere lo stato di Compose con gli oggetti non gestiti da Compose, utilizza il
SideEffect
componibile. 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, come nel caso in cui l'effetto venga scritto direttamente in un 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 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 Compose in stato Compose
produceState
avvia una coroutine con ambito alla composizione che può inserire valori in uno
State restituito. Utilizzalo per convertire lo stato non Compose in stato Compose, ad esempio per portare nella composizione lo stato esterno basato su abbonamento, come Flow, LiveData o RxJava.
Il produttore viene avviato quando produceState entra nella composizione e verrà annullato quando esce dalla composizione. Lo 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 le origini dati non sospese. Per rimuovere l'abbonamento a questa origine, utilizza
la
awaitDispose
funzione.
L'esempio seguente mostra come utilizzare produceState per caricare un'immagine dalla rete. La funzione componibile loadNetworkImage restituisce uno 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 di 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 di stato o un input potrebbe cambiare più spesso di quanto l'UI debba effettivamente aggiornarsi, causando una ricomposizione non necessaria.
Devi utilizzare la derivedStateOf
funzione quando gli input di un componibile cambiano più spesso di quanto sia necessario
ricomporre. Questo si verifica spesso quando qualcosa cambia frequentemente, ad esempio una posizione di scorrimento, ma il componibile deve reagire solo quando supera una determinata soglia. derivedStateOf crea un nuovo oggetto di stato Compose che puoi osservare e che si aggiorna solo quando necessario. In questo modo, si comporta
in modo simile all'operatore
distinctUntilChanged()
di Kotlin Flows.
Uso 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.
Uso non corretto
Un errore comune è presupporre che, quando combini due oggetti di stato Compose, devi utilizzare derivedStateOf perché stai "derivando lo stato". Tuttavia, questo è un overhead puramente 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 una ricomposizione eccessiva 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 al suo interno. Quando uno degli oggetti Stateletti all'interno del blocco snapshotFlowviene modificato, il Flow emetterà il nuovo valore al suo raccoglitore se il nuovo valore non è uguale aquello emesso in precedenza (questo comportamento è simile a quello diFlow.distinctUntilChanged).
L'esempio seguente mostra un effetto collaterale che registra quando l'utente scorre oltre il primo elemento di un elenco per l'analisi:
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 flusso.
Riavviare gli 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.
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:
- Il riavvio degli effetti meno di quanto dovrebbe potrebbe causare bug nell'app.
- Il riavvio degli effetti più di quanto dovrebbe 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 componibile 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 racchiusa
in rememberUpdatedState. Se la variabile non cambia mai perché è racchiusa in un remember senza chiavi, non devi passare la variabile come chiave all'effetto.
Nel codice DisposableEffect mostrato sopra, l'effetto accetta come parametro il lifecycleOwner utilizzato nel suo blocco, perché qualsiasi modifica 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é i loro valori non cambiano mai nella composizione a causa dell'utilizzo di rememberUpdatedState. Se non passi lifecycleOwner come parametro e cambia, HomeScreen viene ricomposto, ma DisposableEffect non viene eliminato e riavviato. Ciò causa problemi perché da quel momento in poi viene utilizzato il lifecycleOwner errato.
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, pensaci bene e assicurati che sia quello che ti serve.
Consigliati per te
- Nota: il testo del link viene visualizzato quando JavaScript è disattivato
- Stato e Jetpack Compose
- Kotlin per Jetpack Compose
- Utilizzare le visualizzazioni in Compose