Corrige problemas de estabilidad

Cuando te enfrentes a una clase inestable que cause problemas de rendimiento, debes optimizar la forma en que Compose la compara o crea una instancia de ella. En este documento, se describen varias técnicas que puedes usar para hacerlo.

Habilita la omisión avanzada

El modo de omisión avanzada está habilitado de forma predeterminada a partir de Kotlin 2.0.20. El modo de omisión avanzado permite omitir los elementos componibles con parámetros inestables comparando los parámetros inestables con la igualdad de instancias (===), mientras que los parámetros estables se siguen comparando con la igualdad estructural (Object.equals()).

Si usas una versión de Kotlin anterior a la 2.0.20, primero habilita el modo de omisión potente. Si el modo de omisión estricto ya está habilitado y una función de componibilidad aún se está recomponiendo de forma innecesaria, verifica si se asignan instancias nuevas de parámetros inestables en cada pase de recomposición.

Consulta Omisión sólida para obtener más información.

Haz que la clase sea inmutable

También puedes hacer que una clase inestable sea completamente inmutable para que el compilador pueda inferir su estabilidad automáticamente.

  • Inmutable: Indica un tipo en el que el valor de cualquier propiedad nunca puede cambiar después de que se construye una instancia de ese tipo, y todos los métodos son referencialmente transparentes.
    • Asegúrate de que todas las propiedades de la clase sean val en lugar de var y de tipos inmutables.
    • Los tipos primitivos, como String, Int y Float, siempre son inmutables.
    • Los tipos data class planos con propiedades primitivas val o String en módulos compilados con el compilador de Compose ya se infieren como estables y se comparan con .equals() automáticamente. No es necesario que los anotes con @Immutable.
    • Si la inmutabilidad es imposible, debes usar el estado de Compose para cualquier propiedad mutable.
  • Estable: Indica un tipo que es mutable. El entorno de ejecución de Compose no se da cuenta si alguna de las propiedades públicas del tipo o el comportamiento del método arrojaría resultados diferentes de una invocación anterior, ni cuándo lo haría.

Recopilaciones y estabilidad

Un motivo común por el que Compose considera que una clase es inestable son las colecciones. Como se indica en la página Diagnostica problemas de estabilidad, el compilador de Compose no puede estar completamente seguro de que las interfaces de colección, como List, Map y Set, sean verdaderamente inmutables y, por lo tanto, las marca como inestables.

Considera esta clase de la guía Diagnostica problemas de estabilidad:

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

Con el modo de omisión avanzada habilitado (el valor predeterminado en Kotlin 2.0.20 y versiones posteriores), las colecciones y clases inestables que contienen colecciones ya omiten la recomposición con la igualdad de instancias (===) cada vez que se pasa la misma referencia.

Ten cuidado antes de reemplazar kotlin.collections.List, Set o Map por colecciones inmutables de Kotlinx (ImmutableList, ImmutableSet, ImmutableMap) o de agregar reglas @Immutable, @Stable o stability_config.conf a los modelos que contienen colecciones. En el modo de omisión estricto, las funciones List estándar se omiten con la igualdad de referencia (===), mientras que ImmutableList y los modelos de colección anotados ejecutan comparaciones .equals() elemento por elemento en cada pase de recomposición.

En el caso de colecciones grandes o diseños diferidos (LazyColumn, LazyRow, LazyGrid), verificar .equals() en cada elemento puede llevar más tiempo que volver a componer. Este costo se duplica si pasas una propiedad ImmutableList de un elemento data class a un elemento componible secundario:

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)
}

En lugar de forzar la igualdad estructural en las colecciones, conserva la referencia de la instancia de la colección en todas las emisiones (por ejemplo, almacenando en caché las transformaciones en tu ViewModel o con remember) para que Compose pueda omitir el uso de la igualdad de instancias (===). Considera las anotaciones de estabilidad o ImmutableList en los modelos que contienen colecciones solo cuando no puedas conservar la referencia de la instancia de la colección y las comparativas confirmen que las verificaciones de igualdad estructural son más rápidas que la recomposición. Si usas una versión de Kotlin anterior a la 2.0.20, primero habilita el modo de omisión estricto.

Anota con Stable o Immutable

Una posible forma de resolver los problemas de estabilidad de las clases pequeñas con verificaciones de .equals() económicas es anotar las clases inestables con @Stable o @Immutable.

Anotar una clase anula lo que, de lo contrario, el compilador inferiría sobre ella. Es similar al operador !! en Kotlin. Debes tener mucho cuidado con la forma en que usas estas anotaciones. Anular el comportamiento del compilador podría generar errores imprevistos, como que tu elemento componible no se vuelva a componer cuando esperas que lo haga.

Con el modo de omisión estricto habilitado, sigue estos lineamientos cuando decidas si agregar @Stable o @Immutable:

  • No anotes clases que ya se infieren como estables: Las clases con propiedades val de tipos estables en módulos compilados con el compilador de Compose ya se infieren como estables.
  • Solo anota clases con verificaciones .equals() económicas cuando se vuelvan a crear las instancias: Agrega @Stable o @Immutable cuando el compilador no pueda inferir la estabilidad (por ejemplo, interfaces o clases de módulos que no sean de Compose) y una fuente de datos asigne nuevas instancias de objetos para elementos sin cambios en cada emisión, siempre que la clase sea pequeña y tenga una verificación .equals() económica.
  • Ten cuidado con los modelos que contienen colecciones: Evita agregar @Stable o @Immutable a los modelos de datos que contienen colecciones grandes (List, Set, Map), en especial para los modelos que se usan en diseños diferidos (LazyColumn, LazyRow, LazyGrid, Pager). Si dejas esos modelos sin anotaciones, Compose puede compararlos con la igualdad de instancias (===) en lugar de ejecutar verificaciones estructurales de .equals() en cada elemento.

En el siguiente fragmento, se proporciona un ejemplo mínimo de una clase de datos plana (sin propiedades de colección) anotada como inmutable cuando se define en un módulo en el que el compilador no puede inferir la estabilidad:

@Immutable
data class SnackSummary(
    val id: Long,
    val name: String,
    val price: Long
)

Ya sea que uses la anotación @Immutable o @Stable, el compilador de Compose marca la clase SnackSummary como estable y la compara con Object.equals().

Clases anotadas en colecciones

Considera un elemento componible que incluye un parámetro de 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
)

Incluso si Snack es estable o está anotado con @Immutable, el compilador de Compose sigue marcando el parámetro snacks en HighlightedSnacks como unstable porque List es una interfaz.

Con el modo de omisión avanzada habilitado, HighlightedSnacks se sigue marcando como skippable y omite la recomposición con la igualdad de instancias (===) cuando se pasa la misma instancia de List. Puedes hacer que el parámetro snacks sea stable uniendo List<Snack> en una clase de wrapper @Immutable, reemplazando List<Snack> por ImmutableList<Snack> o agregando kotlin.collections.* a tu archivo de configuración de estabilidad.

Sin embargo, si lo haces, Compose ejecutará una comparación .equals() elemento por elemento en cada pase de recomposición. Usa estos enfoques solo cuando no puedas conservar la referencia de la instancia de la lista y las comparativas muestren un beneficio.

Archivo de configuración de estabilidad

A partir del compilador de Compose 1.5.5, se puede proporcionar un archivo de configuración de clases que se consideren estables en el tiempo de compilación. Esto permite considerar estables las clases que no controlas, como las clases de biblioteca estándar, como LocalDateTime.

El archivo de configuración es un archivo de texto sin formato con una clase por fila. Se admiten comentarios, comodines simples y dobles.

A continuación, se muestra un ejemplo de configuración:

// 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<*,_>

Para habilitar esta función, pasa la ruta de acceso del archivo de configuración al bloque de opciones composeCompiler de la configuración del complemento de Gradle del compilador de Compose.

composeCompiler {
  stabilityConfigurationFile = rootProject.layout.projectDirectory.file("stability_config.conf")
}

Como el compilador de Compose se ejecuta en cada módulo de tu proyecto por separado, puedes proporcionar diferentes configuraciones a diferentes módulos si es necesario. Como alternativa, puedes tener una configuración en el nivel raíz de tu proyecto y pasar esa ruta de acceso a cada módulo.

Varios módulos

Otro problema común involucra la arquitectura de varios módulos. El compilador de Compose solo puede inferir si una clase es estable si todos los tipos no primitivos a los que hace referencia están marcados explícitamente como estables o en un módulo que también se compiló con el compilador de Compose.

Si tu capa de datos se encuentra en un módulo independiente de la capa de la IU, que es el enfoque recomendado, es posible que encuentres este problema. En el modo de omisión potente, las clases de módulos que no son de Compose se tratan como inestables y, aun así, omiten la recomposición con la igualdad de instancias (===) cuando se pasa la misma instancia. Sin embargo, si tu capa de datos asigna con frecuencia instancias nuevas de modelos pequeños y planos con valores idénticos en cada emisión, puedes habilitar la igualdad estructural (Object.equals()).

Solución

Para resolver este problema, puedes adoptar uno de los siguientes enfoques:

  1. Agrega clases pequeñas y planas con verificaciones de .equals() económicas a tu archivo de configuración del compilador.
  2. Habilita el compilador de Compose en tus módulos de capa de datos o etiqueta tus clases con @Stable o @Immutable según corresponda.
    • Esto implica agregar una dependencia de Compose a tu capa de datos. Sin embargo, es solo la dependencia del entorno de ejecución de Compose y no de Compose-UI.
  3. Dentro de tu módulo de IU, une tus clases de capa de datos en clases de wrapper específicas de la IU.

El mismo problema también se produce cuando se usan bibliotecas externas si no usan el compilador de Compose.

No todos los elementos que admiten composición deben poder omitirse

Cuando trabajes para solucionar problemas de estabilidad, no debes intentar que todas las funciones de componibilidad se puedan omitir ni forzar la igualdad estructural en cada parámetro. Omitir un elemento componible no es gratis: el entorno de ejecución de Compose almacena cada parámetro de un elemento componible que se puede omitir como si se recordara, y Compose debe comparar cada parámetro con su valor anterior cada vez que la función se vuelve a componer para determinar si se debe omitir. Intentar forzar la omisión en todas partes puede generar una optimización prematura que introduzca más sobrecarga de la que ahorra.

Hay muchas situaciones en las que forzar la estabilidad estructural no tiene ningún beneficio real y puede degradar el rendimiento o la capacidad de mantenimiento. Por ejemplo:

  • Un elemento componible que no se recompone con frecuencia o que no se recompone en absoluto
  • Un elemento componible que, en sí mismo, solo llama a elementos componibles que se pueden omitir.
  • Un elemento de componibilidad con una gran cantidad de parámetros o modelos de colección con implementaciones de .equals() costosas En este caso, el costo de verificar si algún parámetro cambió puede superar el costo de una recomposición económica.

No te enfoques demasiado solo en los recuentos de recomposición. Enfócate en hacer que los elementos componibles sean livianos para ejecutar, aplazar las lecturas de estado que cambian rápidamente a las fases de diseño o dibujo, y verificar los cambios con comparativas. Cuando un elemento componible se puede omitir, agrega una pequeña sobrecarga que puede no valer la pena. Incluso puedes anotar tu elemento componible para que no se pueda reiniciar o no se pueda omitir en los casos en que determines que reiniciar o omitir es más costoso de lo que vale.