Seguir las mejores prácticas

Es posible que encuentres errores comunes de Compose. Es posible que estos errores te proporcionen un código que parece ejecutarse lo suficientemente bien, pero que puede afectar el rendimiento de la IU. Sigue las prácticas recomendadas para optimizar tu app en Compose.

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 esa manera, el cálculo se ejecutará una vez y podrás 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 recompone ContactsList, toda la lista de contactos se vuelve a ordenar por completo, aunque la 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, comparator) {
        contacts.sortedWith(comparator)
    }

    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 reutilizan elementos de manera eficiente y solo los vuelve a generar o componer cuando sea necesario. Sin embargo, puedes ayudar a optimizar los diseños diferidos para la recomposición.

Supongamos que una operación del usuario causa que un elemento se mueva en la lista. Por ejemplo, supongamos que muestras una lista de notas ordenadas por hora de modificación con la nota modificada más recientemente 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 los elementos sin cambios solo se muestren en la lista. En cambio, Compose cree que se borró el "elemento 2" anterior y se creó uno nuevo para el elemento 3, el elemento 4 y todos los anteriores. Como resultado, Compose vuelve a componer todos los elementos de la lista, a pesar de que solo uno de ellos cambió en realidad.

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

@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 desplazable. Examinas el estado de la lista para ver qué elemento es el primero visible en ella:

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, ya que 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 cambie el primer elemento visible. Cuando cambia ese valor de estado, la IU debe volver a componerse, pero si el usuario aún no se desplazó lo suficiente como para traer un elemento nuevo a la parte superior, no tiene que volver a componer.

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. A modo de referencia, consulta la implementación en la app de ejemplo de Jetsnack. Jetsnack implementa un efecto similar a la barra de herramientas que se contrae en su pantalla de detalles. Para comprender por qué funciona esta técnica, consulta la entrada de blog Jetpack Compose: Debugging Recomposition.

Para lograr este efecto, el elemento Title componible necesita el desplazamiento de desplazamiento para desplazarse con un Modifier. Esta es una versión simplificada del código de Jetsnack antes de que se realice 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 invalida el alcance de recomposición superior más cercano. En este caso, el alcance más cercano es el elemento SnackDetail componible. Ten en cuenta que Box es una función intercalada, por lo que no es un alcance de recomposición. Por lo tanto, Compose vuelve a componer SnackDetail y cualquier elemento 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 recomponerse.

@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, usa 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 las fases de composición y diseño por completo: 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</b>
}

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 provoca una recomposición, el contador aumenta rápidamente en un bucle infinito a medida que Compose recompone este elemento componible, ve una lectura de estado desactualizada y 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.

Recursos adicionales