Es posible que encuentres 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. 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 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, 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é.
Pasa siempre como claves las entradas a las que se hace referencia dentro del bloque de cálculo remember para que el valor almacenado en caché se actualice cuando cambien esas entradas. Debido a que remember almacena sus claves y el resultado almacenado en caché en la tabla de ranuras y compara las claves con Object.equals() en cada pase de recomposición, solo usa remember cuando almacenes en caché cálculos costosos o asignaciones de objetos pesados (como formateadores de fecha, sombreadores o clasificación de listas) o cuando se deba conservar la identidad del objeto.
Ten cuidado cuando pases una colección grande o un objeto con una implementación de equals() costosa como clave para remember para un cálculo económico, ya que comparar la clave puede costar más que ejecutar el cálculo directamente:
@Composable
fun ContactHeader(contacts: List<Contact>) {
// Avoid: Comparing the `contacts` list with .equals() on recomposition
// is more expensive than reading contacts.size or contacts.firstOrNull() directly.
val firstContactName = remember(contacts) {
contacts.firstOrNull()?.name.orEmpty()
}
Text(firstContactName)
}
Usa claves de diseño diferido y tipos de contenido
Los diseños diferidos reutilizan los elementos de manera eficiente, y solo los vuelven a generar o componer cuando es necesario. Sin embargo, puedes ayudar a optimizar los diseños diferidos para la recomposición y la reutilización de ranuras.
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 determinar 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) } } }
Cuando trabajes con diseños diferidos (LazyColumn, LazyRow, LazyGrid), también ten en cuenta los siguientes lineamientos:
- Usa claves únicas que se puedan guardar en un Bundle: Las claves deben ser únicas, determinísticas y se deben poder guardar en un
Bundlede Android, ya que los diseños diferidos usanrememberSaveablede forma interna. Se prefieren los tipos primitivos, comoInt,LongoString, y nunca se deben usar índices de lista nihashCode()como claves. - Especifica siempre
contentTypepara las listas heterogéneas: PasacontentType = { it.type }aitemsoitemsIndexedpara que Compose solo reutilice las ranuras de composición entre los elementos que comparten la misma estructura visual durante el desplazamiento. - Usa
itemsIndexeden lugar deindexOf: Evita llamar alist.indexOf(item)dentro de un bloqueitems, lo que convierte el diseño en un análisisO(N²). En su lugar, usaitemsIndexed. - Eleva el estado compartido y las lecturas de
CompositionLocal: Recopila instancias deFlowcompartidas y lee los valores deCompositionLocal(comoLocalContext.currentoLocalDensity.current) una vez en el elemento componible superior que se encuentra por encima del diseño diferido y pasa los valores resueltos a los elementos componibles de los elementos. - Usa
RowoColumnpara listas pequeñas y limitadas: En el caso de colecciones estáticas o limitadas pequeñas (como de 1 a 10 elementos), unRowoColumnestándar conforEachevita la virtualización diferida y la sobrecarga de carga previa.
Usa computedStateOf o 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 superior. 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 (o computedStateOf en el entorno de ejecución de Compose 1.13 y versiones posteriores). 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() }
Elige el enfoque de derivación de estado adecuado según tu versión de Compose y el costo de cálculo:
remember { computedStateOf { ... } }(Compose Runtime 1.13 y versiones posteriores): Vuelve a evaluar su cálculo en cada lectura sin almacenar en caché el resultado y solo invalida la composición cuando cambia el resultado calculado. Debido a que evita la contabilidad de la caché y la sobrecarga de escritura de invalidación, prefiereremember { computedStateOf { listState.firstVisibleItemIndex > 0 } }para las verificaciones de condiciones básicas y las derivaciones ligeras impulsadas por estados que cambian con rapidez (como los desplazamientos de desplazamiento).remember { derivedStateOf { ... } }: Almacena en caché el resultado de su cálculo en varias lecturas. UsaderivedStateOfen versiones de Compose anteriores a la 1.13 o cuando el cálculo en sí sea costoso desde el punto de vista computacional y se lea varias veces durante un pase de composición. No pases los valores de estado de la instantánea que se leen dentro dederivedStateOf(comolistState.firstVisibleItemIndex) como claves deremember, ya que, de esta manera, se vuelve a crearDerivedStateen cada cambio de estado y se evita la omisión.- Métodos get de Kotlin simples (
get()) y lecturas de propiedades directas: Elige un método get personalizado simple para expresiones booleanas triviales en las que no se necesite la amortiguación de invalidación y lee la colección.sizedirectamente en lugar de unirla enderivedStateOf. Evita usarproduceStateconsnapshotFlowpara derivaciones de estado síncronas, ya que agrega una sobrecarga innecesaria de envío de corrutinas.
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 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 Jetpack Compose: Depuración de la recomposición.
Para lograr este efecto, la función de componibilidad Title necesita la compensación de desplazamiento, de modo que se pueda desplazar por sí misma 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 invalida el alcance de recomposición superior más cercano. En este caso, el alcance más cercano es el elemento componible SnackDetail. 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 que admita composición 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, 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 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 { mutableIntStateOf(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 de la función de componibilidad 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.
Para obtener una guía detallada sobre las escrituras hacia atrás, los bucles de recomposición y la coordinación de fases, consulta Escrituras hacia atrás en Compose.
Recursos adicionales
- Guía de rendimiento de la app: Descubre las prácticas recomendadas, las bibliotecas y las herramientas para mejorar el rendimiento en Android.
- Inspecciona el rendimiento: Inspecciona el rendimiento de la app.
- Comparativas: Realiza comparativas del rendimiento de la app.
- Inicio de apps: Optimiza el inicio de la app.
- Perfiles de Baseline: Comprende los perfiles de Baseline.
Recomendaciones para ti
- Nota: El texto del vínculo se muestra cuando JavaScript está desactivado
- El estado y Jetpack Compose
- Modificadores de gráficos
- Cómo pensar en Compose