Risolvi i problemi di stabilità

Quando si ha a che fare con una classe instabile che causa problemi di prestazioni, è necessario renderla stabile. Questo documento descrive diverse tecniche che puoi utilizzare per farlo.

Attivare lo strong skipping

Innanzitutto, prova ad attivare la modalità di strong skipping. La modalità di strong skipping consente di saltare gli elementi componibili con parametri instabili ed è il metodo più semplice per risolvere i problemi di prestazioni causati dalla stabilità.

Per ulteriori informazioni, consulta Strong skipping.

Rendere la classe immutabile

Puoi anche provare a rendere una classe instabile completamente immutabile.

  • 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 sia val anziché var sia di tipi immutabili.
    • I tipi primitivi come String, Int e Float sono sempre immutabili.
    • Se non è possibile, devi utilizzare lo stato di Compose per tutte le proprietà modificabili.
  • Stabile: indica un tipo modificabile. Il runtime di Compose non viene a conoscenza se e quando il comportamento di una proprietà pubblica o di un metodo del tipo produrrebbe risultati diversi da una chiamata precedente.

Raccolte immutabili

Un motivo comune per cui Compose considera una classe instabile sono le raccolte. Come indicato nella pagina Diagnosticare i problemi di stabilità, il compilatore Compose non può essere completamente sicuro che le raccolte come List, Map, e Set siano veramente immutabili e quindi le contrassegna come instabili.

Per risolvere il problema, puoi utilizzare raccolte immutabili. Il compilatore Compose include il supporto per le raccolte immutabili Kotlinx. Queste raccolte sono progettate per essere immutabili e il compilatore Compose le tratta come tali. Questa libreria è ancora in versione alfa, quindi prevedi possibili modifiche alla sua API.

Considera di nuovo questa classe instabile dalla guida Diagnosticare i problemi di stabilità:

unstable class Snack {
  
  unstable val tags: Set<String>
  
}

Puoi rendere tags stabile utilizzando una raccolta immutabile. Nella classe, modifica il tipo di tags in ImmutableSet<String>:

data class Snack{
    
    val tags: ImmutableSet<String> = persistentSetOf()
    
}

In questo modo, tutti i parametri della classe sono immutabili e il compilatore Compose contrassegna la classe come stabile.

Aggiungere l'annotazione Stable o Immutable

Un possibile modo per risolvere i problemi di stabilità è aggiungere l'annotazione @Stable o @Immutable alle classi instabili.

L'aggiunta di un'annotazione a una classe sostituisce ciò che il compilatore dedurrebbe altrimenti sulla classe. È simile all' !! operatore in Kotlin. Devi prestare molta attenzione a come utilizzi queste annotazioni. La sostituzione del comportamento del compilatore potrebbe causare bug imprevisti, ad esempio la mancata ricomposizione dell'elemento componibile quando te lo aspetti.

Se è possibile rendere stabile la classe senza un'annotazione, dovresti cercare di ottenere la stabilità in questo modo.

Lo snippet seguente fornisce un esempio minimo di una classe di dati con l'annotazione immutabile:

@Immutable
data class Snack(

)

Indipendentemente dall'utilizzo dell'annotazione @Immutable o @Stable, il compilatore Compose contrassegna la classe Snack come stabile.

Classi con annotazioni nelle raccolte

Considera un elemento componibile che include un parametro di tipo List<Snack>:

restartable scheme("[androidx.compose.ui.UiComposable]") fun HighlightedSnacks(
  
  unstable snacks: List<Snack>
  
)

Anche se aggiungi l'annotazione @Immutable a Snack, il compilatore Compose contrassegna comunque il parametro snacks in HighlightedSnacks come instabile.

I parametri presentano lo stesso problema delle classi quando si tratta di tipi di raccolta: il compilatore Compose contrassegna sempre un parametro di tipo List come instabile, anche quando si tratta di una raccolta di tipi stabili.

Non puoi contrassegnare un singolo parametro come stabile, né puoi aggiungere un'annotazione a un elemento componibile in modo che sia sempre ignorabile. Esistono più modi per procedere.

Esistono diversi modi per aggirare il problema delle raccolte instabili. Le sottosezioni seguenti descrivono questi diversi approcci.

File di configurazione

Se sei soddisfatto del contratto di stabilità nel tuo codebase, allora puoi scegliere di considerare le raccolte Kotlin come stabili aggiungendo kotlin.collections.* al tuo file di configurazione della stabilità.

Raccolta immutabile

Per la sicurezza dell'immutabilità in tempo di compilazione, puoi utilizzare una raccolta immutabile kotlinx anziché List.

@Composable
private fun HighlightedSnacks(
    
    snacks: ImmutableList<Snack>,
    
)

Wrapper

Se non puoi utilizzare una raccolta immutabile, puoi crearne una tua. Per farlo, racchiudi List in una classe stabile con annotazioni. A seconda dei requisiti, la scelta migliore è probabilmente un wrapper generico.

@Immutable
data class SnackCollection(
   val snacks: List<Snack>
)

Puoi quindi utilizzarlo come tipo del parametro nell'elemento componibile.

@Composable
private fun HighlightedSnacks(
    index: Int,
    snacks: SnackCollection,
    onSnackClick: (Long) -> Unit,
    modifier: Modifier = Modifier
)

Soluzione

Dopo aver adottato uno di questi approcci, il compilatore Compose contrassegna l'elemento componibile HighlightedSnacks come skippable e restartable.

restartable skippable scheme("[androidx.compose.ui.UiComposable]") fun HighlightedSnacks(
  stable index: Int
  stable snacks: ImmutableList<Snack>
  stable onSnackClick: Function1<Long, Unit>
  stable modifier: Modifier? = @static Companion
)

Durante la ricomposizione, Compose può ora saltare HighlightedSnacks se nessuno dei suoi input è cambiato.

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 le classi della libreria standard come LocalDateTime, possono essere considerate stabili.

Il file di configurazione è un file di testo con una classe per riga. Sono supportati commenti, 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 composeCompiler blocco di opzioni del plug-in Gradle del compilatore Compose configurazione.

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 multi-modulo. 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 in un modulo creato anche con il compilatore Compose.

Se il livello dati si trova in un modulo separato dal livello UI, che è l'approccio consigliato, potresti riscontrare questo problema.

Soluzione

Per risolvere questo problema, puoi adottare uno dei seguenti approcci:

  1. Aggiungi le classi al file di configurazione del compilatore.
  2. Attiva il compilatore Compose sui moduli del livello dati o aggiungi l'annotazione @Stable o @Immutable alle classi, se appropriato.
    • Ciò comporta l'aggiunta di una dipendenza di Compose al livello dati. Tuttavia, si tratta solo della dipendenza per il runtime di Compose e non per Compose-UI.
  3. All'interno del modulo UI, racchiudi le classi del livello dati in classi wrapper specifiche dell'UI.

Lo stesso problema si verifica anche quando si utilizzano librerie esterne se non utilizzano il compilatore Compose.

Non tutti gli elementi componibili devono essere ignorabili

Quando lavori per risolvere i problemi di stabilità, non devi tentare di rendere ignorabile ogni elemento componibile. Se provi a farlo, potresti ottenere un'ottimizzazione prematura che introduce più problemi di quelli che risolve.

Esistono molte situazioni in cui l'ignorabilità non comporta alcun vantaggio reale e può portare a un codice difficile da gestire. Ad esempio:

  • Un elemento componibile che non viene ricomposto spesso o mai.
  • Un elemento componibile che di per sé chiama solo elementi componibili ignorabili.
  • Un elemento componibile con un numero elevato di parametri con implementazioni di uguaglianza costose. In questo caso, il costo del controllo della modifica di un parametro potrebbe superare il costo di una ricomposizione economica.

Quando un elemento componibile è ignorabile, aggiunge un piccolo overhead che potrebbe non valere la pena. Puoi anche aggiungere un'annotazione all'elemento componibile in modo che non sia riavviabile nei casi in cui determini che la riavviabilità comporta un overhead maggiore del suo valore.