Estabilidad en Compose

Compose considera que los tipos son estables o inestables. Un tipo es estable si es inmutable o si es posible que Compose sepa si su valor cambió entre recomposiciones. Un tipo es inestable si Compose no puede saber si su valor cambió entre recomposiciones.

Compose usa la estabilidad de los parámetros de una función de componibilidad para determinar cómo comparar las entradas y decidir si puede omitir la función de componibilidad durante la recomposición (con el modo de omisión estricto habilitado de forma predeterminada a partir de Kotlin 2.0.20):

  • Parámetros estables: Compose compara los parámetros estables con la igualdad estructural (Object.equals()). Si los parámetros estables de un elemento componible son iguales a sus valores anteriores, Compose los omite.
  • Parámetros inestables: Con el modo de omisión avanzada habilitado (el valor predeterminado en Kotlin 2.0.20 y versiones posteriores), Compose compara los parámetros inestables con la igualdad de instancias (===) y omite el elemento componible si se pasan las mismas instancias de objeto. En las versiones anteriores a Kotlin 2.0.20 (o cuando el modo de omisión avanzada está inhabilitado), Compose siempre vuelve a componer un elemento que admite composición con parámetros inestables cuando se vuelve a componer su elemento superior.

Si tu app asigna con frecuencia instancias nuevas de parámetros inestables o fuerza verificaciones de .equals() costosas en colecciones grandes anotando en exceso los modelos, es posible que observes recomposiciones innecesarias o sobrecarga de comparación.

En este documento, se detalla cómo Compose determina la estabilidad y cómo puedes optimizarla para mejorar el rendimiento y la experiencia general del usuario.

Objetos inmutables

En los siguientes fragmentos, se muestran los principios generales detrás de la estabilidad y la recomposición.

La clase Contact es una clase de datos inmutable. Esto se debe a que todos sus parámetros son primitivas definidas con la palabra clave val. Una vez que creas una instancia de Contact, no puedes cambiar el valor de las propiedades del objeto. Si intentaras hacerlo, crearías un objeto nuevo.

data class Contact(val name: String, val number: String)

El elemento ContactRow que admite composición tiene un parámetro de tipo Contact.

@Composable
fun ContactRow(contact: Contact, modifier: Modifier = Modifier) {
   var selected by remember { mutableStateOf(false) }

   Row(modifier) {
      ContactDetails(contact)
      ToggleButton(selected, onToggled = { selected = !selected })
   }
}

Considera lo que sucede cuando el usuario hace clic en el botón de activación y cambia el estado de selected:

  1. Compose evalúa si debe recomponer el código dentro de ContactRow.
  2. Ve que el único argumento para ContactDetails es del tipo Contact.
  3. Debido a que Contact es una clase de datos inmutable, Compose se asegura de que no haya cambiado ninguno de los argumentos de ContactDetails.
  4. Por lo tanto, Compose omite ContactDetails y no lo vuelve a componer.
  5. Por otro lado, los argumentos de ToggleButton cambiaron, y Compose recompone ese componente.

Objetos mutables

Si bien en el ejemplo anterior se usa un objeto inmutable, es posible crear un objeto mutable. Considera el siguiente fragmento:

data class Contact(var name: String, var number: String)

Como cada parámetro de Contact ahora es un var, la clase ya no es inmutable. Si sus propiedades cambiaran, Compose no se daría cuenta. Esto se debe a que Compose solo hace un seguimiento de los cambios en los objetos State de Compose.

Compose considera que esa clase es inestable. Con el modo de omisión estricto (Kotlin 2.0.20 y versiones posteriores), Compose compara Contact con la igualdad de instancias (===): mutar contact.name in situ no activará la recomposición, mientras que pasar una instancia de Contact recién asignada con valores idénticos seguirá forzando la recomposición de ContactDetails (y, sin el modo de omisión estricto, ContactDetails se recompone cada vez que cambia selected).

Implementación en Compose

Puede ser útil, aunque no crucial, considerar cómo Compose determina exactamente qué funciones omitir durante la recomposición.

Cuando el compilador de Compose se ejecuta en tu código, marca cada función y tipo con una de varias etiquetas. Estas etiquetas reflejan cómo Compose controla la función o el tipo durante la recomposición.

Funciones

Compose puede marcar las funciones como skippable o restartable. Ten en cuenta que puede marcar una función como una de estas, ambas o ninguna:

  • Skippable: Si el compilador marca un elemento componible como skippable, Compose puede omitirlo durante la recomposición si todos sus argumentos son iguales a sus valores anteriores. (Con el modo de omisión avanzada en Kotlin 2.0.20 y versiones posteriores, todos los elementos de componibilidad reiniciables se pueden omitir automáticamente).
  • Reiniciable: Una función de componibilidad que es reiniciable sirve como un "permiso" en el que puede comenzar la recomposición. En otras palabras, la función puede ser un punto de entrada para que Compose pueda comenzar a volver a ejecutar el código para la recomposición después de los cambios de estado.

Tipos

Compose marca los tipos como inmutables o estables. Cada tipo es uno o el otro:

  • Inmutable: Compose marca un tipo como inmutable si el valor de sus propiedades nunca puede cambiar y todos los métodos son transparentes de forma referencial.
    • Ten en cuenta que todos los tipos primitivos se marcan como inmutables. Entre ellas, se incluyen String, Int y Float.
  • Estable: Indica un tipo cuyas propiedades pueden cambiar después de la construcción. Si esas propiedades cambian durante el tiempo de ejecución, Compose se dará cuenta de esos cambios.

Depura la estabilidad

Si tu app está recomponiendo un elemento que admite composición cuyos parámetros no cambiaron, primero verifica si se están asignando instancias nuevas de tipos inestables en cada recomposición (o, si el modo de omisión fuerte está inhabilitado, si el elemento que admite composición tiene parámetros con propiedades var o propiedades val de un tipo inestable).

Para obtener información detallada sobre cómo diagnosticar problemas complejos de estabilidad en Compose, consulta la guía Depura la estabilidad.

Soluciona problemas de estabilidad

Para obtener información sobre cómo brindar estabilidad a tu implementación de Compose, consulta la guía Soluciona problemas de estabilidad.

Resumen

En general, debes tener en cuenta los siguientes puntos:

  • Parámetros: Compose determina la estabilidad de cada parámetro de tus elementos componibles para decidir si los comparará con la igualdad estructural (.equals()) o la igualdad de instancias (===) durante la recomposición.
  • Correcciones inmediatas: Si notas que no se omite tu elemento componible y que esto causa un problema de rendimiento, verifica si se vuelven a crear instancias nuevas de parámetros inestables en cada pase o si se usan propiedades var en lugar de State.
  • Informes del compilador: Puedes usar los informes del compilador para determinar qué estabilidad se infiere sobre tus clases.
  • Colecciones: Compose considera que las interfaces de colección estándar (List, Set y Map) son inestables porque sus implementaciones subyacentes pueden ser mutables. Con el modo de omisión estricto (habilitado de forma predeterminada en Kotlin 2.0.20 y versiones posteriores), las colecciones inestables siguen omitiendo la recomposición con la igualdad de instancias (===) cuando la referencia de la colección no cambia. Ten cuidado cuando reemplaces colecciones estándar por colecciones inmutables de Kotlinx o cuando anotes clases que contengan colecciones con @Immutable o @Stable, ya que comparar cada elemento con .equals() en cada pase de recomposición puede ser más costoso que la recomposición en sí.
  • Otros módulos: Compose considera que las clases de los módulos en los que no se ejecuta el compilador de Compose son inestables (se comparan con === en el modo de omisión estricto). Si una fuente de datos vuelve a crear instancias de modelos pequeños y planos de módulos que no son de Compose con valores idénticos, puedes configurar un archivo de configuración de estabilidad o usar @Stable o @Immutable donde .equals() sea económico.

Lecturas adicionales