Sigue las prácticas recomendadas

Organiza tus páginas con colecciones Guarda y categoriza el contenido según tus preferencias.

Es posible que encuentres algunos errores comunes de Compose. Estos errores pueden brindarte un código que parece ejecutarse lo suficientemente bien, pero que puede afectar el rendimiento de la IU. En esta sección, se incluyen algunas prácticas recomendadas para ayudarte a evitarlos.

Usa remember para minimizar los cálculos costosos

Las funciones de componibilidad pueden ejecutarse con mucha frecuencia, con la misma que para cada fotograma de una animación. Por esta razón, debes hacer el menor cálculo posible en el cuerpo de tu elemento que admite composición.

Una técnica importante es almacenar los resultados de los cálculos con remember. De esta manera, el cálculo se ejecuta una vez y puedes recuperar los resultados cuando sea necesario.

Por ejemplo, a continuación, te mostramos parte de un código que muestra una lista ordenada de nombres, pero que la ordena de forma muy costosa:

@Composable
fun ContactList(
    contacts: List<Contact>,
    comparator: Comparator<Contact>,
    modifier: Modifier = Modifier
) {
    LazyColumn(modifier) {
        // DON’T DO THIS
        items(contacts.sortedWith(comparator)) { contact ->
            // ...
        }
    }
}

Cada vez que se vuelve a componer ContactsList, la lista de contactos completa se vuelve a ordenar en su totalidad, aunque esa lista no haya cambiado. Si el usuario se desplaza por la lista, el elemento componible se vuelve a componer cada vez que aparece una fila nueva.

Para resolver este problema, ordena la lista fuera de LazyColumn y almacena la lista ordenada con remember:

@Composable
fun ContactList(
    contacts: List<Contact>,
    comparator: Comparator<Contact>,
    modifier: Modifier = Modifier
) {
    val sortedContacts = remember(contacts, sortComparator) {
        contacts.sortedWith(sortComparator)
    }

    LazyColumn(modifier) {
        items(sortedContacts) {
          // ...
        }
    }
}

Ahora, la lista se ordena una vez, cuando ContactList se compone por primera vez. Si los contactos o el comparador cambian, se vuelve a generar la lista ordenada. De lo contrario, el elemento que admite composición puede continuar usando la lista ordenada en caché.

Usa claves de diseño diferido

Los diseños diferidos hacen todo lo posible para volver a utilizar los elementos, de manera inteligente, solo volviendo a generar o componer cuando sea necesario. Sin embargo, puedes ayudar a tomar las mejores decisiones.

Supongamos que una operación del usuario causa que un elemento se mueva en la lista. Por ejemplo, imagina que muestras una lista de notas ordenadas según la hora de modificación, con la última nota modificada en la parte superior.

@Composable
fun NotesList(notes: List<Note>) {
    LazyColumn {
        items(
            items = notes
        ) { note ->
            NoteRow(note)
        }
    }
}

Sin embargo, hay un problema con este código. Supongamos que se cambia la nota inferior. Ahora es la última nota modificada, por lo que va a la parte superior de la lista, y todas las demás notas se mueven un lugar hacia abajo.

Sin tu ayuda, Compose no se da cuenta de que solo se mueven elementos sin cambios en la lista. En su lugar, Compose cree que se borró el "elemento 2" anterior y se creó uno nuevo para el elemento 3, el elemento 4 y todos los demás hasta el final de la lista. El resultado es que Compose vuelve a componer todos los elementos de la lista, aunque, en realidad, solo cambie uno de ellos.

La solución aquí es proporcionar claves de elemento. Proporcionar una clave estable para cada elemento permite que Compose evite recomposiciones innecesarias. En este caso, Compose puede ver que el elemento ahora en el punto 3 es el mismo que solía estar en el punto 2. Dado que ninguno de los datos de ese elemento cambió, Compose no tiene que volver a componerlo.

@Composable
fun NotesList(notes: List<Note>) {
    LazyColumn {
        items(
            items = notes,
             key = { note ->
                // Return a stable, unique key for the note
                note.id
            }
        ) { note ->
            NoteRow(note)
        }
    }
}

Usa derivedStateOf para limitar las recomposiciones

Un riesgo de uso del estado en tus composiciones es que, si el estado cambia con rapidez, es posible que la IU se vuelva a componer más de lo necesario. Por ejemplo, supongamos que muestras una lista en la que se puede hacer desplazamientos. Examinas el estado de la lista para ver qué elemento es el primero visible en esta:

val listState = rememberLazyListState()

LazyColumn(state = listState) {
    // ...
}

val showButton = listState.firstVisibleItemIndex > 0

AnimatedVisibility(visible = showButton) {
    ScrollToTopButton()
}

El problema es que, si el usuario se desplaza por la lista, listState cambia de forma constante, a medida que arrastra el dedo. Es decir, la lista se vuelve a componer de manera constante. Sin embargo, en realidad, no necesitas volver a componer con tanta frecuencia; no necesitas hacerlo hasta que un elemento nuevo se vuelva visible en la parte inferior. Por lo tanto, se realiza una gran cantidad de cálculos adicionales, lo que causa que tu IU tenga un rendimiento deficiente.

La solución es usar el estado derivado. El estado derivado te permite indicarle a Compose qué cambios de estado deberían activar la recomposición. En este caso, especifica que te interesa cuándo cambia el primer elemento visible. Cuando cambia ese valor de estado, la IU se debe volver a componer. Sin embargo, si el usuario aún no se desplazó lo suficiente para traer un nuevo elemento a la parte superior, no tiene que volver a componerse.

val listState = rememberLazyListState()

LazyColumn(state = listState) {
  // ...
  }

val showButton by remember {
    derivedStateOf {
        listState.firstVisibleItemIndex > 0
    }
}

AnimatedVisibility(visible = showButton) {
    ScrollToTopButton()
}

Aplaza las lecturas el mayor tiempo posible

Cuando se identifica un problema de rendimiento, aplazar las lecturas de estado puede ayudar. Hacer esto garantizará que Compose vuelva a ejecutar el código mínimo posible en la recomposición. Por ejemplo, si la IU tiene un estado elevado en el árbol de componibilidad y lees el estado en un elemento componible secundario, puedes unir esa lectura de estado en una función lambda. De esta manera, la lectura ocurrirá solo cuando en realidad se necesite. Puedes consultar cómo aplicamos este enfoque a la app de ejemplo de Jetsnack. Jetsnack implementa un efecto similar a la barra de herramientas cuando se contrae en su pantalla de detalles. Para comprender por qué funciona esta técnica, consulta la entrada de blog: Depuración de la recomposición.

Para lograr este efecto, el elemento Title que admite composición debe conocer la compensación de desplazamiento, de modo que se pueda desplazar por sí mismo con Modifier. A continuación, se muestra una versión simplificada del código de Jetsnack, antes de realizar la optimización:

@Composable
fun SnackDetail() {
    // ...

    Box(Modifier.fillMaxSize()) { // Recomposition Scope Start
        val scroll = rememberScrollState(0)
        // ...
        Title(snack, scroll.value)
        // ...
    } // Recomposition Scope End
}

@Composable
private fun Title(snack: Snack, scroll: Int) {
    // ...
    val offset = with(LocalDensity.current) { scroll.toDp() }

    Column(
        modifier = Modifier
            .offset(y = offset)
    ) {
        // ...
    }
}

Cuando cambia el estado de desplazamiento, Compose busca el alcance de recomposición superior más cercano y lo invalida. En este caso, el alcance más cercano es el elemento componible SnackDetail. Nota: Box es una función intercalada, por lo que no actúa como un alcance de recomposición. Por lo tanto, Compose vuelve a componer SnackDetail y también cualquier elemento que componible dentro de SnackDetail. Si cambias tu código para que solo lea el estado en el que realmente lo usas, podrías reducir la cantidad de elementos que deben volver a componerse.

@Composable
fun SnackDetail() {
    // ...

    Box(Modifier.fillMaxSize()) { // Recomposition Scope Start
        val scroll = rememberScrollState(0)
        // ...
        Title(snack) { scroll.value }
        // ...
    } // Recomposition Scope End
}

@Composable
private fun Title(snack: Snack, scrollProvider: () -> Int) {
    // ...
    val offset = with(LocalDensity.current) { scrollProvider().toDp() }
    Column(
        modifier = Modifier
            .offset(y = offset)
    ) {
    // ...
    }
}

El parámetro de desplazamiento ahora es lambda. Es decir, Title aún puede hacer referencia al estado elevado, pero el valor solo se lee dentro de Title, donde en realidad se necesita. Como resultado, cuando cambia el valor de desplazamiento, el alcance de la recomposición más cercano ahora es el elemento Title que admite composición. Compose ya no necesita volver a componer todo el elemento Box.

Esta es una gran cambio, pero puedes hacerlo mejor. Debes sospechar si generas la recomposición solo para volver a diseñar o dibujar un elemento que admite composición. En este caso, lo único que debes hacer es cambiar el desplazamiento del elemento Title que admite composición, lo que podría hacerse en la fase de diseño.

@Composable
private fun Title(snack: Snack, scrollProvider: () -> Int) {
    // ...
    Column(
        modifier = Modifier
            .offset { IntOffset(x = 0, y = scrollProvider()) }
    ) {
      // ...
    }
}

Anteriormente, el código usaba Modifier.offset(x: Dp, y: Dp), que toma el desplazamiento como parámetro. Si cambias a la versión lambda del modificador, puedes asegurarte de que la función lea el estado de desplazamiento en la fase de diseño. Como resultado, cuando cambia el estado de desplazamiento, Compose puede omitir la fase de composición por completo y pasar directamente a la fase de diseño. Cuando, con frecuencia, pasas variables de estado que cambian a modificadores, debes usar las versiones lambda de los modificadores siempre que sea posible.

Este es otro ejemplo de este enfoque. Este código aún no se optimizó:

// Here, assume animateColorBetween() is a function that swaps between
// two colors
val color by animateColorBetween(Color.Cyan, Color.Magenta)

Box(Modifier.fillMaxSize().background(color))

En este código, el color del fondo del cuadro cambia rápidamente entre dos colores. Por lo tanto, este estado cambia con mucha frecuencia. Luego, el elemento que admite composición lee este estado en el modificador en segundo plano. Como resultado, el cuadro debe volver a componerse en cada fotograma, ya que el color cambia en cada uno.

Para mejorar esto, podemos usar un modificador basado en lambda, en este caso, drawBehind. Es decir, el estado de color solo se lee durante la fase de dibujo. Como resultado, Compose puede omitir por completo las fases de composición y diseño; cuando cambia el color, Compose va directamente a la fase de dibujo.

val color by animateColorBetween(Color.Cyan, Color.Magenta)
Box(
   Modifier
      .fillMaxSize()
      .drawBehind {
         drawRect(color)
      }
)

Evita las escrituras hacia atrás

Compose tiene la suposición principal de que nunca escribirás en un estado que ya se haya leído. Cuando lo haces, se denomina escritura hacia atrás, y puede producirse una recomposición en todos los fotogramas, de forma infinita.

El siguiente elemento componible muestra un ejemplo de este tipo de error.

@Composable
fun BadComposable() {
    var count by remember { mutableStateOf(0) }

    // Causes recomposition on click
    Button(onClick = { count++ }, Modifier.wrapContentSize()) {
        Text("Recompose")
    }

    Text("$count")
    count++ // Backwards write, writing to state after it has been read
}

Este código actualiza el recuento al final del elemento componible después de leerlo en la línea anterior. Si ejecutas este código, verás que, después de hacer clic en el botón, lo que causa una recomposición, el contador aumenta con rapidez en un bucle infinito, ya que Compose vuelve a componer este elemento componible, detecta una lectura de estado desactualizada y, por lo tanto, programa otra recomposición.

Puedes evitar la escritura hacia atrás por completo si nunca escribes en el estado en Composition. Si es posible, escribe siempre en el estado en respuesta a un evento y en una lambda, como en el ejemplo anterior de onClick.