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 di ricomposizioni di elementi componibili in ordini diversi o le ricomposizioni che possono essere scartate, gli elementi componibili dovrebbero 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 il passaggio a un'altra schermata in base a una determinata condizione di stato. Queste azioni dovrebbero essere richiamate da un ambiente controllato, consapevole del ciclo di vita dell'elemento componibile. In questa pagina, scoprirai le diverse API con effetto collaterale offerte da Jetpack Compose.
Casi d'uso degli stati e degli effetti
Come indicato nella documentazione di Thinking in Compose, i componibili non dovrebbero 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 gli effetti collaterali vengano eseguiti in modo prevedibile.
Date le diverse possibilità che gli effetti si aprono in Compose, possono essere facilmente abusati. Assicurati che il lavoro che esegui al loro interno sia correlato all'interfaccia utente 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 chiamare le funzioni di sospensione in modo sicuro dall'interno di un componibile, utilizza l'elemento componibile LaunchedEffect
. Quando LaunchedEffect
entra nella composizione, avvia una
coroutine con il blocco di codice trasmesso come parametro. La coroutine verrà annullata se LaunchedEffect
lascia la composizione. Se LaunchedEffect
viene
ricomposto con chiavi diverse (consulta la sezione Effetti di riavvio di seguito), la coroutine esistente verrà annullata e la nuova funzione di sospensione verrà lanciata in una nuova.
Ad esempio, per mostrare una Snackbar
in una Scaffold
puoi utilizzare la funzione SnackbarHostState.showSnackbar
, che è una funzione di sospensione.
@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 -> // ... } }
Nel codice riportato sopra, una coroutine viene attivata se lo stato contiene un errore e
viene annullata in caso contrario. Poiché il sito di chiamata LaunchedEffect
si trova all'interno di
un'istruzione if, quando l'affermazione è falsa, se LaunchedEffect
si trovava nella
composizione, verrà rimossa e, pertanto, la coroutine verrà annullata.
rememberCoroutineScope
: ottieni un ambito sensibile alla composizione per lanciare 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 ambito in modo che venga annullata automaticamente quando lascia la
composizione, utilizza
rememberCoroutineScope
.
Usa 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 è chiamato. L'ambito verrà annullato quando la chiamata abbandona la 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 che non deve essere riavviato se il valore cambia
LaunchedEffect
si riavvia quando viene modificato uno dei parametri chiave. Tuttavia, in
alcune situazioni potresti voler acquisire un valore nell'effetto che, se cambia, non vuoi che si riavvii. A questo scopo, è necessario utilizzare rememberUpdatedState
per creare un riferimento a questo valore che possa essere acquisito e aggiornato. Questo approccio è utile per gli effetti che contengono operazioni di lunga durata che possono essere costose o proibitive da ricreare e riavviare.
Ad esempio, supponi che la tua app abbia un valore LandingScreen
che scompare dopo un po' di tempo. Anche se LandingScreen
viene ricomposto, l'effetto che attende un po' di tempo
e avvisa 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 della chiamata, viene passata come parametro una costante in continuo cambiamento, come Unit
o true
. Nel
codice riportato sopra, viene utilizzato LaunchedEffect(true)
. Per assicurarti che la lambda onTimeout
contenga sempre il valore più recente con cui è stata ricomposta LandingScreen
, devi racchiudere onTimeout
nella funzione rememberUpdatedState
.
Il valore State
restituito, currentOnTimeout
nel codice, deve essere utilizzato nell'effetto.
DisposableEffect
: effetti che richiedono pulizia
Per gli effetti collaterali che devono essere puliti dopo la modifica dei tasti o se il componibile lascia la composizione, utilizza DisposableEffect
.
Se i tasti DisposableEffect
cambiano, il componibile deve smaltire (eseguire la pulizia) l'effetto attuale e resettarlo richiamando di nuovo 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'osservatore 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 elemento DisposableEffect
deve includere una clausola onDispose
come istruzione finale
nel blocco di codice. In caso contrario, l'IDE mostra un errore in fase di build.
SideEffect
: pubblica lo stato di Compose in un codice non in scrittura
Per condividere lo stato della Scrittura con oggetti non gestiti da Compose, utilizza l'elemento componibile 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 è il caso in cui si scrive l'effetto direttamente in un componibile.
Ad esempio, la libreria di analisi potrebbe consentirti di segmentare la popolazione di utenti collegando i metadati personalizzati ("proprietà utente" in questo esempio) a tutti i successivi eventi di analisi. Per comunicare il tipo di utente dell'utente corrente 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 Compose nello stato di Compose
produceState
avvia una coroutine con ambito alla composizione che può inviare valori in una
State
restituita. Utilizzalo per
convertire lo stato non Scrittura nello stato Scrittura, ad esempio portando nella
Composizione uno stato esterno basato sull'abbonamento, come Flow
, LiveData
o RxJava
.
Il produttore viene lanciato quando produceState
entra nella composizione e verrà annullato quando esce dalla composizione. Il valore State
restituito si confonde;
l'impostazione dello stesso valore non attiva una ricomposizione.
Anche se produceState
crea una coroutine, può essere utilizzato anche per osservare
le fonti di dati non sospese. Per rimuovere l'abbonamento 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
: converti uno o più oggetti di stato in un altro stato
In Compose, la ricomposizione si verifica ogni volta che un oggetto di stato osservato o un input componibile cambia. Un input o un oggetto di stato potrebbero cambiare più spesso di quanto l'aggiornamento della UI debba effettivamente essere aggiornato, causando una ricomposizione non necessaria.
Dovresti usare la funzione derivedStateOf
quando gli input in un componibile cambiano più spesso del necessario per ricomporre. Questo si verifica spesso quando qualcosa cambia spesso, 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 si aggiorna solo quanto necessario. In questo modo, agisce in modo simile all'operatore Kotlin Flows distinctUntilChanged()
.
Utilizzo corretto
Lo snippet seguente 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 il primo elemento visibile cambia. Mentre 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 Compose,
dovresti utilizzare derivedStateOf
perché stai ottenendo lo "stato derivato". Tuttavia, questo è puramente overhead 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, l'aggiornamento fullName
deve essere aggiornato con la stessa frequenza di firstName
e
lastName
. Pertanto, non viene eseguita alcuna ricomposizione in eccesso e non è necessario utilizzare
derivedStateOf
.
snapshotFlow
: converti lo stato di Compose in Flows
Utilizza snapshotFlow
per convertire gli oggetti State<T>
in un flusso a freddo. snapshotFlow
esegue il 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 Flow emette il nuovo valore al raccoglitore se il nuovo valore non è uguale al valore emesso precedente (questo comportamento è simile a quello di Flow.distinctUntilChanged
).
L'esempio seguente mostra un effetto collaterale che registra quando l'utente scorre il primo elemento di un elenco su 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ò trarre vantaggio dalla potenza degli operatori di Flow.
Effetti di riavvio
Alcuni effetti in Compose, come LaunchedEffect
, produceState
o
DisposableEffect
, prevedono un numero variabile di argomenti e chiavi utilizzati per
annullare l'effetto in esecuzione e avviarne uno nuovo con le nuove chiavi.
Il formato tipico di queste API è:
EffectName(restartIfThisKeyChanges, orThisKey, orThisKey, ...) { block }
A causa delle sfumature di questo comportamento, possono verificarsi problemi se i parametri utilizzati per riavviare l'effetto non sono quelli corretti:
- Il riavvio degli effetti meno previsto potrebbe causare bug nella tua app.
- Gli effetti del riavvio potrebbero essere inefficienti.
Come regola generale, le variabili mutabili e immutabili utilizzate nel blocco degli effetti del codice devono essere aggiunte come parametri all'effetto componibile. Puoi 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 aggregata in rememberUpdatedState
. Se la variabile non cambia mai perché è aggregata in un remember
senza chiavi, non è necessario passare la variabile come chiave all'effetto.
Nel codice DisposableEffect
mostrato sopra, l'effetto assume come parametro lifecycleOwner
utilizzato nel blocco, poiché 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 necessarie come chiavi DisposableEffect
, poiché il loro valore non cambia mai in Composizione a causa dell'utilizzo di
rememberUpdatedState
. Se non passi come parametro lifecycleOwner
e
cambia, HomeScreen
si ricompone, ma DisposableEffect
non viene eliminato
e riavviato. Questo causa problemi perché da quel momento in poi viene utilizzato il valore lifecycleOwner
sbagliato.
Costanti come chiavi
Puoi utilizzare una costante come true
come chiave effetto per seguire il ciclo di vita del sito della 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.
Consigliato per te
- Nota: il testo del link viene visualizzato quando JavaScript è disattivato
- State e Jetpack Compose
- Kotlin per Jetpack Compose
- Utilizzare le visualizzazioni in Compose