Corrige problemas de estabilidad

Cuando te enfrentes a una clase inestable que causa problemas de rendimiento, debes hacerla estable. En este documento, se describen varias técnicas que puedes usar para hacerlo.

Habilitar la función de omisión fuerte

Primero debes intentar habilitar el modo de omisión seguro. El modo de omisión sólido permite que se omitan los elementos componibles con parámetros inestables. Además, es el método más fácil para solucionar problemas de rendimiento causados por la estabilidad.

Consulta Saltos repentinos para obtener más información.

Cómo hacer que la clase sea inmutable

También puedes intentar hacer que una clase inestable sea completamente inmutable.

  • 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 referenciamente transparentes.
    • Asegúrate de que todas las propiedades de la clase sean val en lugar de var y que sean de tipos inmutables.
    • Los tipos primitivos, como String, Int y Float, siempre son inmutables.
    • Si esto 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 sabe si alguna de las propiedades públicas o el comportamiento del método del tipo generaría resultados diferentes de los de una invocación anterior y cuándo.

Colecciones inmutables

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

Para resolver esto, puedes usar colecciones inmutables. El compilador de Compose admite las colecciones inmutables de Kotlinx. Se garantiza que estas colecciones son inmutables, y el compilador de Compose las trata como tales. Esta biblioteca aún se encuentra en versión alfa, por lo que se esperan posibles cambios en su API.

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

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

Puedes hacer que tags sea estable con una colección inmutable. En la clase, cambia el tipo de tags a ImmutableSet<String>:

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

Después de hacerlo, todos los parámetros de la clase son inmutables, y el compilador de Compose la marca como estable.

Anota con Stable o Immutable

Una posible ruta para resolver los problemas de estabilidad es anotar las clases inestables con @Stable o @Immutable.

La anotación de una clase anula lo que el compilador inferiría sobre tu clase. Es similar al operador !! en Kotlin. Debes tener mucho cuidado al usar estas anotaciones. La anulación del comportamiento del compilador podría generar errores imprevistos, por ejemplo, que el elemento componible no se vuelva a componer cuando lo esperas.

Si es posible que tu clase sea estable sin una anotación, debes esforzarte para lograr la estabilidad de esa manera.

En el siguiente fragmento, se proporciona un ejemplo mínimo de una clase de datos anotada como inmutable:

@Immutable
data class Snack(
…
)

Ya sea que uses la anotación @Immutable o @Stable, el compilador de Compose marca la clase Snack como estable.

Clases anotadas en colecciones

Considera un elemento componible que incluya un parámetro de tipo List<Snack>:

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

Incluso si anotas Snack con @Immutable, el compilador de Compose aún marca el parámetro snacks en HighlightedSnacks como inestable.

Los parámetros enfrentan el mismo problema que las clases cuando se trata de tipos de colección, el compilador de Compose siempre marca un parámetro de tipo List como inestable, incluso cuando es una colección de tipos estables.

No puedes marcar un parámetro individual como estable ni anotar un elemento componible para que siempre se pueda omitir. Hay varias rutas hacia delante.

Existen varias formas de evitar el problema de las colecciones inestables. En las siguientes subsecciones, se describen estos diferentes enfoques.

Archivo de configuración

Si estás de acuerdo con cumplir con el contrato de estabilidad de tu base de código, puedes optar por considerar las colecciones de Kotlin como estables. Para ello, agrega kotlin.collections.* a tu archivo de configuración de estabilidad.

Recopilación inmutable

Para garantizar la seguridad del tiempo de compilación de la inmutabilidad, puedes usar una colección inmutable de Kotlinx, en lugar de List.

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

Wrapper

Si no puedes usar una colección inmutable, puedes crear la tuya. Para ello, une el List en una clase estable con anotaciones. Según tus requisitos, es probable que un wrapper genérico sea la mejor opción para esto.

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

Luego, puedes usarlo como el tipo del parámetro en el elemento componible.

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

Solución

Después de tomar cualquiera de estos enfoques, el compilador de Compose ahora marca el elemento HighlightedSnacks componible como skippable y 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 recomposición, Compose ahora puede omitir HighlightedSnacks si ninguna de sus entradas cambió.

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. De esta manera, puedes considerar las clases que no controlas, como las clases de biblioteca estándar (como LocalDateTime), como estables.

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

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

Para habilitar esta función, pasa la ruta de acceso del archivo de configuración a las opciones del compilador de Compose.

Groovy

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

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 habitual se relaciona con 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 está en un módulo separado de la capa de la IU, que es el enfoque recomendado, es posible que este sea un problema.

Solución

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

  1. Agrega las clases a tu archivo de configuración del compilador.
  2. Habilita el compilador de Compose en tus módulos de capas de datos o etiqueta tus clases con @Stable o @Immutable cuando 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 del módulo de la IU, une las clases de capas de datos en clases de wrapper específicas de la IU.

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

No todos los elementos componibles deben poder omitirse

Cuando intentes solucionar problemas de estabilidad, no intentes hacer que todos los elementos componibles se puedan omitir. Si lo intentas, es posible que se genere una optimización prematura que introduzca más problemas de los que corrige.

Hay muchas situaciones en las que el formato de anuncio que se puede omitir no tiene ningún beneficio real y puede dificultar el mantenimiento del código. Por ejemplo:

  • Un elemento componible que no se recompone con frecuencia o que no se recompone.
  • Es un elemento componible que, por sí solo, llama a los elementos que se pueden omitir.
  • Un elemento componible con una gran cantidad de parámetros con implementaciones costosas es igual a En este caso, el costo de verificar si cambió algún parámetro podría superar el costo de una recomposición económica.

Cuando un elemento componible se puede omitir, agrega una pequeña sobrecarga que puede no valer la pena. Incluso puedes anotar un elemento componible para que no se pueda reiniciar en los casos en los que determines que ser reiniciable implica una sobrecarga mayor de lo que merece la pena.