Risolvi i problemi di stabilità

Quando ti trovi di fronte a una classe instabile che causa problemi di prestazioni, dovresti renderla stabile. Questo documento descrive varie tecniche che puoi utilizzare per ottenere questo risultato.

Rendi immutabile il corso

Prima di tutto, dovresti 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 trasparenti a livello di riferimento.
    • Assicurati che tutte le proprietà della classe siano val anziché var e che siano di tipo immutabile.
    • I tipi primitivi come String, Int e Float sono sempre immutabili.
    • Se questo non è possibile, devi utilizzare lo stato Scrivi per tutte le proprietà modificabili.
  • Stabile: indica un tipo modificabile. Il runtime di Compose non rileva se e quando una delle proprietà pubbliche o il comportamento del metodo di un tipo restituisce risultati diversi rispetto a una chiamata precedente.

Raccolte immutabili

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

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

Considera di nuovo questa classe instabile dalla guida Diagnostica 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()
    …
}

In seguito, tutti i parametri della classe saranno 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 sarebbe altrimenti dedotto dalla tua classe. È simile all'operatore !! di Kotlin. Dovresti fare molta attenzione a come usi queste annotazioni. La sostituzione del comportamento del compilatore potrebbe comportare bug imprevisti, ad esempio la mancata ricomposizione della componibile quando previsto.

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

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

@Immutable
data class Snack(
…
)

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

Classi con annotazioni nelle raccolte

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

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

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

Per quanto riguarda i tipi di raccolta, i parametri devono affrontare lo stesso problema delle classi. 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 elemento componibile in modo che sia sempre ignorabile. Esistono più percorsi in avanti.

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

File di configurazione

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

Raccolta immutabile

Per garantire la sicurezza dell'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, puoi crearne una personalizzata. Per farlo, aggrega List in una classe stabile annotata. A seconda dei requisiti, la scelta migliore è probabilmente un wrapper generico.

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

Puoi quindi utilizzarlo come tipo di parametro nella 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, ora Scrivi 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, come 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 è riportata una configurazione di esempio:

// 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 Scrivi.

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 di Compose viene eseguito separatamente su ogni modulo del progetto, puoi fornire configurazioni diverse a moduli diversi, se necessario. In alternativa, imposta una configurazione a livello della directory principale del progetto e passa il percorso a ciascun modulo.

Più moduli

Un altro problema comune riguarda un'architettura multimodulo. Il compilatore 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 livello dati si trova in un modulo separato dal livello dell'interfaccia utente, 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. Nel 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 se non usano il compilatore Composer.

Non tutti i prodotti componibili devono essere ignorabili

Quando cerchi di risolvere problemi di stabilità, non devi cercare di rendere ignorabile ogni elemento componibile. Tentare di farlo può portare a un'ottimizzazione prematura che introduce più problemi di quanti ne consenta la correzione.

Esistono molte situazioni in cui l'ignoramento non offre alcun reale vantaggio e può portare a una gestione del codice complessa. Ecco alcuni esempi:

  • Un prodotto componibile che non viene ricomposto spesso o per niente.
  • Un elemento componibile che di per sé chiama elementi componibili ignorabili.
  • Un componibile con un gran numero di parametri con costose implementazioni di tipo uguale. In questo caso, il costo di verificare la modifica di un parametro può superare il costo di una ricomposizione economica.

Quando un annuncio componibile è ignorabile, aggiunge un piccolo overhead che potrebbe non valere la pena. Puoi persino annotare la tua componibile in modo che sia non riavviabile nei casi in cui stabilisca che essere riavviabile comporta un overhead maggiore del suo valore.