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 prodotti componibili, come le ricomposizioni imprevedibili, l'esecuzione delle ricomposizioni di elementi componibili in ordini diversi o le ricomposizioni che possono essere eliminate, queste ultime dovrebbero idealmente essere prive di effetti collaterali.
Tuttavia, a volte sono necessari effetti collaterali, ad esempio per attivare un evento una tantum, come mostrare una snackbar o passare a un'altra schermata in base a una determinata condizione di stato. Queste azioni dovrebbero essere richiamate da un ambiente controllato che sia a conoscenza del ciclo di vita dell'oggetto componibile. In questa pagina scoprirai le diverse API con effetti collaterali offerte da Jetpack Compose.
Stato ed effetto dei casi d'uso
Come indicato nella documentazione di Thinking in Compose, gli elementi componibili non dovrebbero avere effetti collaterali. Quando devi apportare modifiche allo stato dell'app (come descritto nel documento sulla documentazione sulla gestione dello stato), devi utilizzare le API Effect in modo che gli effetti collaterali vengano eseguiti in modo prevedibile.
A causa delle diverse possibilità che gli effetti si presentano in Compose, potrebbero essere facilmente usati eccessivamente. Assicurati che il lavoro svolto sia relativo all'interfaccia utente e che non interrompa il flusso di dati unidirezionale come spiegato nella documentazione relativa alla gestione dello stato.
LaunchedEffect
: esegui le funzioni di sospensione nell'ambito di un'istanza componibile
Per chiamare le funzioni di sospensione in modo sicuro dall'interno di un componente componibile, utilizza l'oggetto componibile LaunchedEffect
. Quando LaunchedEffect
entra nella composizione, avvia una
coroutine con il blocco di codice passato come parametro. La coroutine verrà
annullata se LaunchedEffect
lascia la composizione. Se LaunchedEffect
viene ricomposta con chiavi diverse (consulta la sezione Effetti di riavvio di seguito), la coroutine esistente verrà annullata e la nuova funzione di sospensione verrà avviata in una nuova coroutine.
Ad esempio, per mostrare una Snackbar
in una Scaffold
puoi usare 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
verrà annullata in caso contrario. Poiché il sito della chiamata LaunchedEffect
si trova all'interno di un'istruzione if, quando l'istruzione è falsa e se LaunchedEffect
era nella composizione, verrà rimosso e, di conseguenza, la coroutina verrà annullata.
rememberCoroutineScope
: ottieni un ambito sensibile alla composizione per lanciare una coroutine al di fuori di una componibile
Poiché LaunchedEffect
è una funzione componibile, può essere utilizzata solo all'interno di altre funzioni componibili. Per avviare una coroutine al di fuori di una componibile,
ma con ambito in modo che venga annullata automaticamente una volta lasciata
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 collegamento CoroutineScope
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 riavviarsi 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 si riavvii. A questo scopo, è
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 possono 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 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 continua evoluzione, come Unit
o true
. Nel codice riportato sopra, viene utilizzato LaunchedEffect(true)
. Per assicurarti che onTimeout
lambda contenga sempre il valore più recente con cui LandingScreen
è stato ricomposto, onTimeout
deve essere aggregato nella funzione rememberUpdatedState
.
State
restituito, currentOnTimeout
nel codice, deve essere utilizzato
nell'effetto.
DisposableEffect
: effetti che richiedono una pulizia
Per gli effetti collaterali che devono essere puliti dopo la modifica delle chiavi o se il componibile lascia la composizione, utilizza
DisposableEffect
.
Se le chiavi DisposableEffect
cambiano, l'oggetto componibile deve eliminare (eseguire la pulizia) l'effetto attuale e reimpostarlo richiamando di nuovo l'effetto.
Ad esempio, potresti inviare eventi di analisi basati su eventi Lifecycle
utilizzando un LifecycleObserver
.
Per ascoltare questi eventi in Compose, 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 il valore 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 della scrittura nel codice diverso da Scrivi
Per condividere lo stato della scrittura con oggetti non gestiti dalla scrittura, utilizza l'oggetto componibile SideEffect
. L'uso 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 si scrive l'effetto direttamente in un elemento 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 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 nello stato di composizione
produceState
avvia una coroutine con ambito a livello di composizione che può trasferire i valori in una
State
restituita. Utilizzalo per
convertire lo stato non in composizione nello stato Scrittura, ad esempio per trasferire nella composizione lo stato
esterno basato sull'abbonamento, come Flow
, LiveData
o RxJava
.
Il produttore viene lanciato quando produceState
entra nella composizione e viene annullato quando lascia la composizione. Il valore State
restituito si combina;
l'impostazione dello stesso valore non attiva una ricomposizione.
Anche se produceState
crea una coroutina, può essere utilizzata anche per osservare
le fonti di dati che non consumano. Per rimuovere l'abbonamento all'origine, utilizza la funzione awaitDispose
.
L'esempio seguente mostra come utilizzare produceState
per caricare un'immagine dalla rete. La funzione componibile loadNetworkImage
restituisce un valore State
che può essere utilizzato in altri elementi 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 Scrivi, 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'interfaccia utente debba effettivamente aggiornare, il che porta a una ricomposizione non necessaria.
Dovresti utilizzare la funzione derivedStateOf
quando gli input in una componibile cambiano più spesso del necessario per la ricomposizione. Questo problema si verifica spesso quando qualcosa cambia di frequente, ad esempio una posizione di scorrimento, ma l'oggetto componibile deve reagire solo quando supera una determinata soglia. derivedStateOf
crea un nuovo oggetto dello stato di composizione che puoi osservare e che si aggiorna solo quanto necessario. In questo modo funziona in modo simile all'operatore distinctUntilChanged()
di Kotlin Flows.
Uso 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 viene modificato il primo elemento visibile. 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 indica che questo è un buon caso d'uso per derivedStateOf
.
Uso non corretto
Un errore comune è presumere che, quando combini due oggetti di stato Compose,
dovresti utilizzare derivedStateOf
perché stai "derivando lo stato". Tuttavia, si tratta puramente di un overhead e non è obbligatorio, 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 eccessiva e non è necessario utilizzare
derivedStateOf
.
snapshotFlow
: converti lo stato della Scrittura in flussi
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
letto all'interno del blocco snapshotFlow
cambia, il flusso emette il nuovo valore nel 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 oltre 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.
Riavvia effetti
Alcuni effetti in Scrivi, come LaunchedEffect
, produceState
o DisposableEffect
, utilizzano un numero variabile di argomenti e chiavi utilizzati per annullare l'effetto applicato e avviarne uno nuovo con le nuove chiavi.
La forma tipica 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:
- Riavviare gli effetti meno del previsto potrebbe causare bug nell'app.
- Riavviare gli effetti più del previsto potrebbe essere inefficiente.
Come regola generale, le variabili modificabili e immutabili utilizzate nel blocco effetto del codice dovrebbero 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 perché è aggregata in un remember
senza chiavi, non è necessario passare la variabile come chiave per l'effetto.
Nel codice DisposableEffect
mostrato sopra, l'effetto assume come parametro 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 necessarie come chiavi DisposableEffect
, perché il loro valore non cambia mai in Composizione a causa dell'utilizzo di rememberUpdatedState
. Se non passi lifecycleOwner
come parametro e
cambia, HomeScreen
si ricompone, 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 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,
ripensaci 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 Scrivi