Risolvi i problemi di stabilità

Quando ci si trova di fronte a una classe instabile che causa problemi di prestazioni, è consigliabile renderla stabile. Questo documento descrive varie tecniche che puoi usare per farlo.

Abilita salto forzato

Per prima cosa, devi provare ad attivare la modalità Ignora forte. Questa modalità consente di ignorare gli elementi componibili con parametri instabili ed è il metodo più semplice per risolvere i problemi di prestazioni causati dalla stabilità.

Per ulteriori informazioni, consulta la sezione Passaggio elevato.

Rendi immutabile il corso

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

Raccolte immutabili

Un motivo comune per cui Compose considera instabili un corso sono le raccolte. Come indicato nella pagina Diagnostica i problemi di stabilità, il compilatore di Compose non può essere completamente sicuro che le raccolte come List, Map e Set siano realmente immutabili e pertanto le contrassegna come instabili.

Per risolvere questo problema, puoi utilizzare le raccolte immutabili. Il compilatore Compose include il supporto per Raccolte immutabili Kotlinx. Queste raccolte sono garantite come immutabili e il compilatore Compose le tratta come tali. Questa libreria è ancora in versione alpha, quindi aspettati possibili modifiche alla relativa API.

Considera di nuovo questa classe instabile nella guida Diagnostica i problemi di stabilità:

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

Puoi rendere stabile tags utilizzando una raccolta immutabile. Nel corso, cambia il tipo di tags in ImmutableSet<String>:

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

Dopodiché, tutti i parametri della classe rimarranno immutabili e il compilatore di Compose contrassegna la classe come stabile.

Annota con Stable o Immutable

Un possibile percorso per risolvere i problemi di stabilità è annotare le classi instabili con @Stable o @Immutable.

L'annotazione di una classe sostituisce ciò che il compilatore avrebbe altrimenti dedotto sulla tua classe. È simile all'operatore !! di Kotlin. Dovresti fare molta attenzione a come usi queste annotazioni. Se esegui l'override del comportamento del compilatore, potresti riscontrare bug imprevisti, ad esempio il tuo componibile che non viene ricomposto quando previsto.

Se è possibile rendere stabile la classe senza un'annotazione, devi impegnarti a raggiungere la stabilità in questo modo.

Lo snippet seguente fornisce un esempio minimo di classe di dati annotata come immutabile:

@Immutable
data class Snack(
…
)

Indipendentemente dal fatto che utilizzi l'annotazione @Immutable o @Stable, il compilatore Compose contrassegna la classe Snack come stabile.

Classi annotate nelle raccolte

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

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

Anche se annoti Snack con @Immutable, il compilatore Compose contrassegna comunque il parametro snacks in HighlightedSnacks come instabile.

I parametri devono affrontare lo stesso problema delle classi per quanto riguarda i 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é annotare un componibile in modo che sia sempre ignorabile. Ci sono più percorsi in avanti.

Esistono diversi modi per risolvere il problema dell'instabilità delle raccolte. Le seguenti sottosezioni descrivono i diversi approcci.

File di configurazione

Se vuoi rispettare il contratto di stabilità nel tuo codebase, puoi scegliere di considerare le raccolte Kotlin come stabili aggiungendo kotlin.collections.* al tuo file di configurazione della stabilità.

Raccolta immutabile

Per garantire l'immutabilità in fase 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, potresti crearne una tua. Per farlo, inserisci List in una classe stabile annotata. Un wrapper generico è probabilmente la scelta migliore, a seconda delle tue esigenze.

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

Puoi quindi utilizzare questo elemento come tipo di parametro nel tuo 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 ora 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 ora può saltare HighlightedSnacks se nessuno dei suoi input è stato modificato.

File di configurazione della stabilità

A partire dalla versione 1.5.5 di Compose Compiler, è possibile fornire un file di configurazione di classi da considerare stabili al momento della compilazione. In questo modo, puoi considerare stabili le classi che non controlli, ad esempio le classi di librerie standard come LocalDateTime.

Il file di configurazione è un file di testo normale con una classe per riga. Sono supportati commenti, caratteri jolly singoli e doppi. Di seguito è riportato un esempio di configurazione:

// Consider LocalDateTime stable
java.time.LocalDateTime
// Consider kotlin collections stable
kotlin.collections.*
// 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 abilitare questa funzionalità, passa il percorso del file di configurazione alle opzioni del compilatore di Compose.

trendy

kotlinOptions {
    freeCompilerArgs += [
            "-P",
            "plugin:androidx.compose.compiler.plugins.kotlin:stabilityConfigurationPath=" +
                    project.absolutePath + "/compose_compiler_config.conf"
    ]
}

Kotlin

kotlinOptions {
  freeCompilerArgs += listOf(
      "-P",
      "plugin:androidx.compose.compiler.plugins.kotlin:stabilityConfigurationPath=" +
      "${project.absolutePath}/compose_compiler_config.conf"
  )
}

Poiché il compilatore Compose viene eseguito separatamente su ogni modulo del tuo progetto, puoi fornire configurazioni diverse a moduli diversi, se necessario. In alternativa, disponi di una configurazione a livello della directory principale del progetto e passa il percorso a ciascun modulo.

Più moduli

Un altro problema comune riguarda l'architettura multi-modulo. Il compilatore di Compose può dedurre solo se una classe è stabile se tutti i tipi non primitivi a cui fa riferimento sono contrassegnati esplicitamente come stabili o in un modulo creato anch'esso con il compilatore Compose.

Se il tuo livello dati si trova in un modulo separato dal livello UI, che è l'approccio consigliato, potrebbe trattarsi di un problema.

Soluzione

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

  1. Aggiungi le classi al file di configurazione del compilatore.
  2. Abilita il compilatore Compose nei moduli del livello dati o tagga le tue classi con @Stable o @Immutable dove appropriato.
    • Ciò comporta l'aggiunta di una dipendenza 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, aggrega le classi del livello dati in classi wrapper specifiche dell'interfaccia utente.

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 problemi di stabilità, non provare a rendere ogni elemento componibile ignorabile. Tentare di farlo può portare a un'ottimizzazione precoce, che genera più problemi di quanti non ne riesca a risolvere.

Ci sono molte situazioni in cui ignorare il codice non ha alcun vantaggio reale e può essere difficile da gestire. Ecco alcuni esempi:

  • Un componibile che non viene ricomposto spesso o per niente.
  • Un componibile che di per sé chiama semplicemente elementi componibili ignorabili.
  • Un componibile con un numero elevato di parametri con implementazioni costose di uguali. In questo caso, il costo per verificare se un parametro è cambiato potrebbe superare il costo di una ricomposizione economica.

Quando un componibile è ignorabile, aggiunge un piccolo sovraccarico che potrebbe non valere la pena. Puoi anche annotare il tuo componibile in modo che sia non riavviabile nei casi in cui stabilisca che il riavvio è più overhead di quanto valga.