Seguir las mejores prácticas

Es posible que encuentres errores comunes de Compose. Estos errores pueden darte código que parece ejecutarse lo suficientemente bien, pero que puede afectar el rendimiento de la IU. Seguir el mejor 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 forma, el cálculo se ejecuta una vez y puedes recuperar los resultados cuando se los necesita.

Por ejemplo, aquí te mostramos parte de un código que muestra una lista ordenada de nombres, pero de ordenación 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 recomponga ContactsList, se ordenará toda la lista de contactos una vez más, aunque la lista no haya cambiado. Si el usuario se desplaza por la lista, el elemento componible se recompone 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 los elementos de manera eficiente, ya que solo se generan o recomponen. cuando sea necesario. Sin embargo, puedes ayudar a optimizar los diseños diferidos a 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 el mayor nota modificada 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 solo se están mostrando elementos sin cambios se movió en la lista. En cambio, Compose considera que el antiguo "elemento 2" se borró y un se creó uno nuevo para el elemento 3, el elemento 4 y todo el extremo inferior. El resultado es que Compose vuelve a componer todos los elementos de la lista, aunque solo uno de ellos. realmente cambió.

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 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 recomponerla.

@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 la lista:

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, no es necesario que lo recompongas con tanta frecuencia no necesitas volver a componer hasta que un nuevo elemento 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 esa cambia el valor del estado, la IU debe recomponerse, pero si el usuario aún no desplazado lo suficiente como para llevar 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. Como referencia, consulta la implementación en el artículo Jetsnack. app de ejemplo. Jetsnack implementa un efecto similar a una barra de herramientas que se contrae en su pantalla de detalles. Para comprender por qué funciona esta técnica, consulte la entrada de blog Jetpack Compose: Recomposición de depuración.

Para lograr este efecto, el elemento componible Title necesita el desplazamiento de desplazamiento. para desplazarse usando 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 elemento superior más cercano. alcance de la recomposición. En este caso, el alcance más cercano es el SnackDetail. componible. Ten en cuenta que Box es una función intercalada y, por lo tanto, no es una recomposición. del proyecto. Por lo tanto, Compose recompone SnackDetail y cualquier elemento componible dentro de ella. SnackDetail Si cambias tu código para que solo lea el estado en el que la utilizas, 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 elemento offset 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 por completo las fases de composición y diseño cuando el color cambios, 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 el línea anterior. Si ejecutas este código, lo verás después de hacer clic en que genera una recomposición, el contador aumenta rápidamente en una bucle infinito a medida que Compose recompone este elemento componible, ve una lectura de estado que se desactualizados, por lo que 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