Potresti riscontrare insidie comuni di Compose. Questi errori potrebbero generare codice che sembra funzionare abbastanza bene, ma possono influire sulle prestazioni dell'interfaccia utente. Segui le best practice per ottimizzare la tua app su Compose.
Utilizza remember per ridurre al minimo i calcoli costosi
Le funzioni componibili possono essere eseguite molto spesso, anche per ogni frame di un'animazione. Per questo motivo, dovresti eseguire il minor numero possibile di calcoli nel corpo del composable.
Una tecnica importante è memorizzare i risultati dei calcoli con
remember. In questo modo, il calcolo viene eseguito una sola volta e puoi recuperare i risultati ogni volta che sono necessari.
Ad esempio, ecco un codice che mostra un elenco ordinato di nomi, ma l'ordinamento viene eseguito in modo molto costoso:
@Composable fun ContactList( contacts: List<Contact>, comparator: Comparator<Contact>, modifier: Modifier = Modifier ) { LazyColumn(modifier) { // DON’T DO THIS items(contacts.sortedWith(comparator)) { contact -> // ... } } }
Ogni volta che ContactsList viene ricomposto, l'intero elenco contatti viene ordinato di nuovo, anche se l'elenco non è cambiato. Se l'utente scorre l'elenco,
il composable viene ricomposto ogni volta che viene visualizzata una nuova riga.
Per risolvere il problema, ordina l'elenco al di fuori di LazyColumn e memorizza
l'elenco ordinato con remember:
@Composable fun ContactList( contacts: List<Contact>, comparator: Comparator<Contact>, modifier: Modifier = Modifier ) { val sortedContacts = remember(contacts, comparator) { contacts.sortedWith(comparator) } LazyColumn(modifier) { items(sortedContacts) { // ... } } }
Ora l'elenco viene ordinato una sola volta, quando ContactList viene composto per la prima volta. Se i
contatti o il comparatore cambiano, l'elenco ordinato viene rigenerato. In caso contrario, il
componente può continuare a utilizzare l'elenco ordinato memorizzato nella cache.
Utilizzare i tasti di layout pigro
I layout pigri riutilizzano in modo efficiente gli elementi, rigenerandoli o ricomponendoli solo quando necessario. Tuttavia, puoi contribuire a ottimizzare i layout pigri per la ricomposizione.
Supponiamo che un'operazione dell'utente provochi lo spostamento di un elemento nell'elenco. Ad esempio, supponiamo che tu mostri un elenco di note ordinate in base all'ora di modifica, con la nota modificata più di recente in alto.
@Composable fun NotesList(notes: List<Note>) { LazyColumn { items( items = notes ) { note -> NoteRow(note) } } }
Si è verificato un problema con questo codice. Supponiamo che la nota in basso venga modificata. Ora è la nota modificata più di recente, quindi viene spostata in cima all'elenco e tutte le altre note vengono spostate di una posizione verso il basso.
Senza il tuo aiuto, Crea non si rende conto che gli elementi invariati vengono semplicemente spostati nell'elenco. Compose ritiene invece che il vecchio "elemento 2" sia stato eliminato e che ne sia stato creato uno nuovo per l'elemento 3, l'elemento 4 e così via. Il risultato è che Compose ricompone ogni elemento dell'elenco, anche se solo uno di questi è effettivamente cambiato.
La soluzione in questo caso è fornire le chiavi degli elementi. Fornire una chiave stabile per ogni elemento consente a Compose di evitare ricomposizioni non necessarie. In questo caso, Compose può determinare che l'elemento ora al terzo posto è lo stesso che si trovava al secondo posto. Poiché nessuno dei dati per quell'elemento è cambiato, Compose non deve ricomporlo.
@Composable fun NotesList(notes: List<Note>) { LazyColumn { items( items = notes, key = { note -> // Return a stable, unique key for the note note.id } ) { note -> NoteRow(note) } } }
Utilizza derivedStateOf per limitare le ricomposizioni
Un rischio dell'utilizzo dello stato nelle composizioni è che, se lo stato cambia rapidamente, la UI potrebbe essere ricomposta più del necessario. Ad esempio, supponiamo che tu stia visualizzando un elenco scorrevole. Esamina lo stato dell'elenco per vedere qual è il primo elemento visibile dell'elenco:
val listState = rememberLazyListState() LazyColumn(state = listState) { // ... } val showButton = listState.firstVisibleItemIndex > 0 AnimatedVisibility(visible = showButton) { ScrollToTopButton() }
Il problema è che, se l'utente scorre l'elenco, listState cambia
continuamente mentre l'utente trascina il dito. Ciò significa che l'elenco viene costantemente
ricomposto. Tuttavia, non è necessario ricomporre così spesso: non è necessario ricomporre finché non diventa visibile un nuovo elemento in basso. Quindi,
si tratta di molti calcoli aggiuntivi, il che peggiora le prestazioni della tua UI.
La soluzione è utilizzare lo stato derivato. Lo stato derivato consente di indicare a Compose quali modifiche dello stato devono effettivamente attivare la ricomposizione. In questo caso, specifica che ti interessa quando cambia il primo elemento visibile. Quando il valore dello stato cambia, l'interfaccia utente deve ricomporsi, ma se l'utente non ha ancora scorri abbastanza da portare un nuovo elemento in primo piano, non deve ricomporsi.
val listState = rememberLazyListState() LazyColumn(state = listState) { // ... } val showButton by remember { derivedStateOf { listState.firstVisibleItemIndex > 0 } } AnimatedVisibility(visible = showButton) { ScrollToTopButton() }
Rimanda le letture il più a lungo possibile
Quando viene identificato un problema di prestazioni, la posticipazione delle letture dello stato può essere utile. Il rinvio delle letture dello stato garantisce che Compose esegua nuovamente il codice minimo possibile durante la ricomposizione. Ad esempio, se la tua UI ha uno stato che viene sollevato in alto nell'albero dei composable e leggi lo stato in un composable secondario, puoi racchiudere la lettura dello stato in una funzione lambda. In questo modo, la lettura viene eseguita solo quando è effettivamente necessaria. Per riferimento, vedi l'implementazione nell'app di esempio Jetsnack. Jetsnack implementa un effetto simile a una barra degli strumenti che si comprime nella schermata dei dettagli. Per capire perché questa tecnica funziona, consulta il post del blog Jetpack Compose: Debugging Recomposition.
Per ottenere questo effetto, il componente componibile Title ha bisogno dell'offset di scorrimento
per compensarsi utilizzando un Modifier. Ecco una versione semplificata del codice
Jetsnack prima dell'ottimizzazione:
@Composable fun SnackDetail() { // ... Box(Modifier.fillMaxSize()) { // Recomposition Scope Start val scroll = rememberScrollState(0) // ... Title(snack, scroll.value) // ... } // Recomposition Scope End } @Composable private fun Title(snack: Snack, scroll: Int) { // ... val offset = with(LocalDensity.current) { scroll.toDp() } Column( modifier = Modifier .offset(y = offset) ) { // ... } }
Quando lo stato di scorrimento cambia, Compose invalida l'ambito di ricomposizione del genitore più vicino. In questo caso, l'ambito più vicino è il composable SnackDetail. Tieni presente che Box è una funzione inline e quindi non è un ambito di ricomposizione. Pertanto, Compose ricompone SnackDetail e tutti i composable all'interno di
SnackDetail. Se modifichi il codice in modo che legga solo lo stato in cui lo utilizzi effettivamente, puoi ridurre il numero di elementi che devono essere ricomposti.
@Composable fun SnackDetail() { // ... Box(Modifier.fillMaxSize()) { // Recomposition Scope Start val scroll = rememberScrollState(0) // ... Title(snack) { scroll.value } // ... } // Recomposition Scope End } @Composable private fun Title(snack: Snack, scrollProvider: () -> Int) { // ... val offset = with(LocalDensity.current) { scrollProvider().toDp() } Column( modifier = Modifier .offset(y = offset) ) { // ... } }
Il parametro di scorrimento ora è una lambda. Ciò significa che Title può ancora fare riferimento allo stato
sollevato, ma il valore viene letto solo all'interno di Title, dove è effettivamente
necessario. Di conseguenza, quando il valore di scorrimento cambia, l'ambito di ricomposizione più vicino è ora il composable Title. Compose non deve più ricomporre l'intero Box.
Questo è un buon miglioramento, ma puoi fare di meglio. Devi insospettirti se
stai causando una ricomposizione solo per riorganizzare o ridisegnare un elemento componibile. In
questo caso, tutto ciò che fai è modificare l'offset del composable Title,
che potrebbe essere fatto nella fase di layout.
@Composable private fun Title(snack: Snack, scrollProvider: () -> Int) { // ... Column( modifier = Modifier .offset { IntOffset(x = 0, y = scrollProvider()) } ) { // ... } }
In precedenza, il codice utilizzava Modifier.offset(x: Dp, y: Dp), che accetta
l'offset come parametro. Se passi alla versione lambda del modificatore,
puoi assicurarti che la funzione legga lo stato di scorrimento nella fase di layout. Di conseguenza, quando lo stato di scorrimento cambia, Compose può saltare completamente la fase di composizione e passare direttamente alla fase di layout. Quando passi variabili di stato che cambiano frequentemente nei modificatori, devi utilizzare le versioni lambda dei modificatori, se possibile.
Ecco un altro esempio di questo approccio. Questo codice non è ancora stato ottimizzato:
// Here, assume animateColorBetween() is a function that swaps between // two colors val color by animateColorBetween(Color.Cyan, Color.Magenta) Box( Modifier .fillMaxSize() .background(color) )
In questo caso, il colore di sfondo del riquadro cambia rapidamente tra due colori. Questo stato cambia quindi molto spesso. Il composable legge quindi questo stato nel modificatore di sfondo. Di conseguenza, la casella deve essere ricomposta a ogni frame, poiché il colore cambia a ogni frame.
Per migliorare questo aspetto, utilizza un modificatore basato su lambda, in questo caso drawBehind.
Ciò significa che lo stato del colore viene letto solo durante la fase di disegno. Di conseguenza,
Compose può saltare completamente le fasi di composizione e layout: quando il colore
cambia, Compose passa direttamente alla fase di disegno.
val color by animateColorBetween(Color.Cyan, Color.Magenta) Box( Modifier .fillMaxSize() .drawBehind { drawRect(color) } )
Evitare le scritture all'indietro
Compose si basa sul presupposto fondamentale che non scriverai mai a uno stato che è già stato letto. Quando lo fai, si parla di scrittura all'indietro e può causare una ricomposizione continua su ogni frame.
Il seguente composable mostra un esempio di questo tipo di errore.
@Composable fun BadComposable() { var count by remember { mutableIntStateOf(0) } // Causes recomposition on click Button(onClick = { count++ }, Modifier.wrapContentSize()) { Text("Recompose") } Text("$count") count++ // Backwards write, writing to state after it has been read</b> }
Questo codice aggiorna il conteggio alla fine del composable dopo averlo letto nella riga precedente. Se esegui questo codice, vedrai che dopo aver fatto clic sul pulsante, che causa una ricomposizione, il contatore aumenta rapidamente in un ciclo infinito man mano che Compose ricompone questo composable, rileva una lettura dello stato obsoleta e quindi pianifica un'altra ricomposizione.
Puoi evitare del tutto le scritture all'indietro non scrivendo mai nello stato in
Composition. Se possibile, scrivi sempre allo stato in risposta a un evento
e in una lambda come nell'esempio onClick precedente.
Per una guida approfondita su scritture all'indietro, cicli di ricomposizione e coordinamento delle fasi, consulta Scritture all'indietro in Compose.
Risorse aggiuntive
- Guida alle prestazioni delle app: scopri best practice, librerie e strumenti per migliorare le prestazioni su Android.
- Ispeziona le prestazioni: ispeziona le prestazioni dell'app.
- Benchmarking: confronta le prestazioni dell'app.
- Avvio dell'app: ottimizza l'avvio dell'app.
- Profili di baseline: scopri di più sui profili di baseline.
Consigliati per te
- Nota: il testo del link viene visualizzato quando JavaScript è disattivato
- Stato e Jetpack Compose
- Modificatori della grafica
- Pensiero in Compose