Quando ti trovi di fronte a una classe instabile che causa problemi di prestazioni, devi ottimizzare il modo in cui Compose la confronta o ne crea un'istanza. Questo documento illustra diverse tecniche che puoi utilizzare per farlo.
Attiva lo skipping avanzato
La modalità di strong skipping è attiva per impostazione predefinita a partire da Kotlin 2.0.20.
La modalità di strong skipping consente di saltare gli elementi componibili con parametri instabili confrontando i parametri instabili utilizzando l'uguaglianza delle istanze (===), mentre i parametri stabili continuano a essere confrontati utilizzando l'uguaglianza strutturale (Object.equals()).
Se utilizzi una versione di Kotlin precedente alla 2.0.20, attiva prima la modalità di skipping avanzato. Se la modalità di strong skipping è già attiva e un composable continua a essere ricomposto inutilmente, controlla se vengono allocate nuove istanze di parametri instabili a ogni passaggio di ricomposizione.
Per saperne di più, consulta Strong skipping.
Rendi la classe immutabile
Puoi anche rendere una classe instabile completamente immutabile in modo che il compilatore possa dedurre automaticamente la sua stabilità.
- Immutabile: indica un tipo in cui il valore di qualsiasi proprietà non può mai cambiare dopo la creazione di un'istanza di quel tipo e tutti i metodi sono referenzialmente trasparenti.
- Assicurati che tutte le proprietà della classe siano
valanzichévare di tipi immutabili. - I tipi primitivi come
String,InteFloatsono sempre immutabili. - I tipi
data classsemplici con proprietà primitivevaloStringnei moduli creati con il compilatore Compose vengono già dedotti come stabili e confrontati automaticamente con.equals(). Non è necessario annotarli con@Immutable. - Se l'immutabilità è impossibile, devi utilizzare lo stato di Compose per tutte le proprietà mutabili.
- Assicurati che tutte le proprietà della classe siano
- Stable: indica un tipo modificabile. Il runtime di Compose non viene a sapere se e quando una delle proprietà pubbliche o un comportamento dei metodi del tipo produrrebbe risultati diversi da una chiamata precedente.
Raccolte e stabilità
Un motivo comune per cui Compose considera una classe instabile sono le raccolte. Come indicato nella pagina Diagnostica i problemi di stabilità, il compilatore Compose non può essere completamente sicuro che le interfacce di raccolta come List, Map e Set siano veramente immutabili e pertanto le contrassegna come instabili.
Considera questa classe della guida Diagnostica i problemi di stabilità:
unstable class Snack {
…
unstable val tags: Set<String>
…
}
Con la modalità di salto avanzata attivata (l'impostazione predefinita in Kotlin 2.0.20 e versioni successive), le raccolte instabili e le classi contenenti raccolte saltano già la ricomposizione utilizzando l'uguaglianza delle istanze (===) ogni volta che viene passato lo stesso riferimento.
Fai attenzione prima di sostituire kotlin.collections.List, Set o Map con
Kotlinx Immutable Collections (ImmutableList, ImmutableSet, ImmutableMap)
o di aggiungere regole @Immutable, @Stable o stability_config.conf ai
modelli che contengono raccolte. In modalità di skipping avanzato, List standard salta
utilizzando l'uguaglianza di riferimento (===), mentre ImmutableList e i modelli di raccolta
annotati eseguono confronti .equals() elemento per elemento in ogni
passaggio di ricomposizione.
Per raccolte di grandi dimensioni o layout lazy (LazyColumn, LazyRow, LazyGrid), il controllo di .equals() su ogni elemento può richiedere più tempo della ricomposizione. Questo costo raddoppia se passi una proprietà ImmutableList da un composable data class a un composable secondario:
data class SnackCategory(
val name: String,
val snacks: ImmutableList<Snack>
)
@Composable
fun CategorySection(category: SnackCategory) {
Text(category.name)
// Compose compares category.snacks twice: once in SnackCategory.equals()
// for CategorySection, and again for HighlightedSnacks.
HighlightedSnacks(snacks = category.snacks)
}
Anziché forzare l'uguaglianza strutturale sulle raccolte, conserva il riferimento all'istanza della raccolta tra le emissioni (ad esempio, memorizzando nella cache le trasformazioni nel tuo ViewModel o con remember) in modo che Compose possa saltare l'utilizzo dell'uguaglianza delle istanze (===). Prendi in considerazione ImmutableList o le annotazioni di stabilità sui modelli che contengono raccolte solo quando non puoi conservare il riferimento all'istanza della raccolta e i benchmark confermano che i controlli di uguaglianza strutturale sono più veloci della ricomposizione.
Se utilizzi una versione di Kotlin precedente alla 2.0.20, attiva prima la modalità di skipping avanzata.
Aggiungi annotazioni con Stable o Immutable
Un possibile percorso per risolvere i problemi di stabilità per le classi di piccole dimensioni con controlli .equals() economici è annotare le classi instabili con @Stable o @Immutable.
L'annotazione di una classe esegue l'override di ciò che il compilatore dedurrebbe altrimenti sulla classe. È simile all'operatore !! in Kotlin. Devi prestare molta attenzione a come utilizzi queste annotazioni. L'override del comportamento del compilatore potrebbe causare bug imprevisti, ad esempio il composable potrebbe non ricomporsi quando previsto.
Con la modalità di salto aggressiva attiva, segui queste linee guida per decidere se aggiungere @Stable o @Immutable:
- Non annotare le classi che sono già dedotte come stabili: le classi con
proprietà
valdi tipi stabili nei moduli creati con il compilatore Compose sono già dedotte come stabili. - Annota le classi con controlli
.equals()economici solo quando le istanze vengono ricreate: aggiungi@Stableo@Immutablequando il compilatore non può dedurre la stabilità (ad esempio, interfacce o classi di moduli non Compose) e un'origine dati alloca nuove istanze di oggetti per elementi invariati a ogni emissione, a condizione che la classe sia piccola e abbia un controllo.equals()economico. - Fai attenzione ai modelli contenenti raccolte: evita di aggiungere
@Stableo@Immutableai modelli di dati che contengono raccolte di grandi dimensioni (List,Set,Map), in particolare per i modelli utilizzati nei layout lazy (LazyColumn,LazyRow,LazyGrid,Pager). Se lasci questi modelli senza annotazioni, Compose può confrontarli utilizzando l'uguaglianza delle istanze (===) anziché eseguire controlli strutturali.equals()su ogni elemento.
Lo snippet seguente fornisce un esempio minimo di una classe di dati flat (senza proprietà di raccolta) annotata come immutabile se definita in un modulo in cui il compilatore non può dedurre la stabilità:
@Immutable
data class SnackSummary(
val id: Long,
val name: String,
val price: Long
)
Indipendentemente dal fatto che utilizzi l'annotazione @Immutable o @Stable, il compilatore Compose contrassegna la classe SnackSummary come stabile e la confronta utilizzando Object.equals().
Classi annotate nelle raccolte
Prendi in considerazione un componibile che include un parametro di tipo List<Snack>:
restartable skippable scheme("[androidx.compose.ui.UiComposable]") fun HighlightedSnacks(
stable index: Int
unstable snacks: List<Snack>
stable onSnackClick: Function1<Long, Unit>
stable modifier: Modifier? = @static Companion
)
Anche se Snack è stabile o annotato con @Immutable, il compilatore Compose contrassegna comunque il parametro snacks in HighlightedSnacks come unstable perché List è un'interfaccia.
Con la modalità di skipping avanzata attivata, HighlightedSnacks è ancora contrassegnato come
skippable e salta la ricomposizione utilizzando l'uguaglianza delle istanze (===) quando viene passata
la stessa istanza List. Puoi rendere il parametro snacks stable
inserendo List<Snack> in una classe wrapper @Immutable, sostituendo List<Snack>
con ImmutableList<Snack> o aggiungendo kotlin.collections.* al tuo file di configurazione
della stabilità.
Tuttavia, in questo modo Compose esegue un confronto elemento per elemento .equals()
in ogni passaggio di ricomposizione. Utilizza questi approcci solo quando non puoi conservare il riferimento all'istanza dell'elenco e i benchmark mostrano un vantaggio.
File di configurazione della stabilità
A partire dal compilatore Compose 1.5.5, è possibile fornire un file di configurazione delle classi da considerare stabili in tempo di compilazione. In questo modo, le classi che non controlli, come quelle della libreria standard come LocalDateTime, vengono considerate stabili.
Il file di configurazione è un file di testo normale con una classe per riga. Sono supportati i commenti e i caratteri jolly singoli e doppi.
Esempio di configurazione:
// Consider LocalDateTime stable
java.time.LocalDateTime
// Consider my datalayer stable
com.datalayer.*
// Consider my datalayer and all submodules stable
com.datalayer.**
// Consider my generic type stable based off it's first type parameter only
com.example.GenericClass<*,_>
Per attivare questa funzionalità, passa il percorso del file di configurazione al blocco di opzioni composeCompiler della configurazione del plug-in Gradle del compilatore Compose.
composeCompiler {
stabilityConfigurationFile = rootProject.layout.projectDirectory.file("stability_config.conf")
}
Poiché il compilatore Compose viene eseguito separatamente su ogni modulo del progetto, puoi fornire configurazioni diverse a moduli diversi, se necessario. In alternativa, puoi avere una configurazione a livello di root del progetto e passare il percorso a ogni modulo.
Più moduli
Un altro problema comune riguarda l'architettura multimodulo. Il compilatore Compose può dedurre se una classe è stabile solo se tutti i tipi non primitivi a cui fa riferimento sono contrassegnati esplicitamente come stabili o si trovano in un modulo creato anche con il compilatore Compose.
Se il livello dati si trova in un modulo separato rispetto al livello UI, che è l'approccio consigliato, questo potrebbe essere un problema che riscontri. In modalità di skipping
forte, le classi dei moduli non Compose vengono trattate come instabili e continuano a
saltare la ricomposizione utilizzando l'uguaglianza delle istanze (===) quando viene passata
la stessa istanza. Tuttavia, se il tuo livello dati alloca spesso nuove istanze di modelli piccoli e piatti con valori identici a ogni emissione, puoi attivare l'uguaglianza strutturale (Object.equals()).
Soluzione
Per risolvere il problema, puoi adottare uno dei seguenti approcci:
- Aggiungi classi piccole e piatte con controlli
.equals()economici al file di configurazione del compilatore. - Attiva il compilatore Compose sui moduli del livello dati o aggiungi i tag
@Stableo@Immutablealle classi, a seconda dei casi.- Ciò comporta l'aggiunta di una dipendenza di Compose al livello dati. Tuttavia,
è solo la dipendenza per il runtime di Compose e non per
Compose-UI.
- Ciò comporta l'aggiunta di una dipendenza di Compose al livello dati. Tuttavia,
è solo la dipendenza per il runtime di Compose e non per
- All'interno del modulo UI, inserisci le classi del livello dati in classi wrapper specifiche per l'UI.
Lo stesso problema si verifica anche quando si utilizzano librerie esterne che non utilizzano il compilatore Compose.
Non tutti i componibili devono essere ignorabili
Quando cerchi di risolvere i problemi di stabilità, non dovresti tentare di rendere ogni componibile ignorabile o di forzare l'uguaglianza strutturale su ogni parametro. La possibilità di saltare un componibile non è priva di costi: ogni parametro di un componibile ignorabile viene archiviato dal runtime di Compose come se fosse memorizzato e Compose deve confrontare ogni parametro con il suo valore precedente ogni volta che la funzione viene ricomposta per determinare se deve essere ignorata. Tentare di forzare l'omissione ovunque può portare a un'ottimizzazione prematura che introduce più overhead di quanto ne risparmi.
Esistono molte situazioni in cui forzare la stabilità strutturale non offre alcun vantaggio reale e può peggiorare le prestazioni o la manutenibilità. Ad esempio:
- Un composable che non viene ricomposto spesso o non viene ricomposto affatto.
- Un composable che di per sé chiama solo composable ignorabili.
- Un composable con un numero elevato di parametri o modelli di raccolta con implementazioni
.equals()costose. In questo caso, il costo del controllo per verificare se un parametro è cambiato può superare il costo di una ricomposizione economica.
Non fare troppo affidamento solo sui conteggi delle ricomposizioni. Concentrati sulla creazione di composabili leggeri da eseguire, rimandando le letture dello stato in rapida evoluzione alle fasi di layout o disegno e verificando le modifiche con i benchmark. Quando un composable è ignorabile, aggiunge un piccolo overhead che potrebbe non valere la pena. Puoi anche annotare il composable come non riavviabile o non ignorabile nei casi in cui ritieni che la riavviabilità o l'ignorabilità comportino un overhead eccessivo.