Resolución práctica de problemas de rendimiento en Jetpack Compose

1. Antes de comenzar

En este codelab, aprenderás a mejorar el rendimiento del tiempo de ejecución de una app de Compose. Seguirás un enfoque científico para medir, depurar y optimizar el rendimiento. También investigarás varios problemas de rendimiento con el registro del sistema y modificarás el código del tiempo de ejecución que no cumple con los requisitos en una app de muestra, que contiene varias pantallas que representan diferentes tareas. Cada pantalla está creada de diferente manera e incluye lo siguiente:

  • La primera pantalla es una lista de dos columnas con imágenes y algunas etiquetas en la parte superior. Aquí es donde optimizas los elementos componibles pesados.

8afabbbbbfc1d506.gif

  • La segunda y la tercera pantalla contienen un estado de recomposición frecuente. Aquí, quitas las recomposiciones innecesarias para optimizar el rendimiento.

f0ccf14d1c240032.gif 51dc23231ebd5f1a.gif

  • La última pantalla contiene elementos inestables. Aquí, estabilizas los elementos con diversas técnicas.

127f2e4a2fc1a381.gif

Requisitos previos

Qué aprenderás

Requisitos

2. Prepárate

Para comenzar, sigue estos pasos:

  1. Clona el repositorio desde GitHub
$ git clone https://github.com/android/codelab-android-compose.git

También tienes la opción de descargar el repositorio como archivo ZIP:

  1. Abre el proyecto PerformanceCodelab, que contiene las siguientes ramas:
  • main: Contiene el código de partida para este proyecto, en el que realizarás cambios para completar el codelab.
  • end: Contiene el código de solución para este codelab.

Te recomendamos que comiences con la rama main y sigas el codelab paso a paso a tu propio ritmo.

  1. Si quieres ver el código de solución, ejecuta este comando:
$ git clone -b end https://github.com/android/codelab-android-compose.git

También puedes descargar el código de solución:

Opcional: Registros del sistema usados en este codelab

Durante el codelab, ejecutarás varias comparativas que capturan los registros del sistema.

Si no puedes ejecutarlas, esta es una lista de registros del sistema que puedes descargar:

3. Enfoque de corrección de problemas de rendimiento

Detectar UIs lentas y que no cumplen con los requisitos de rendimiento es posible a simple vista y con solo explorar la app. Pero antes de empezar de lleno y comenzar a corregir código según suposiciones, deberías medir su rendimiento para comprender si los cambios marcarán una diferencia.

Durante el desarrollo, con una compilación debuggable de la app, es probable que notes cierto incumplimiento en el rendimiento, y comenzar a abordar el problema podría ser una tentación. Sin embargo, un valor debuggable en el rendimiento de la app no representa lo que los usuarios verán, así que es importante verificar con una app non-debuggable que efectivamente se trata de un problema. En una app debuggable, la totalidad del código se tiene que interpretar en función del tiempo de ejecución.

Cuando se considera el rendimiento en Compose, no hay ninguna regla fija que se deba seguir para implementar una funcionalidad específica. No deberías precipitarte en realizar las siguientes acciones:

  • Buscar y corregir cada parámetro inestable que se filtre en el código
  • Quitar animaciones que provoquen la recomposición de elementos componibles
  • Llevar a cabo optimizaciones difíciles de leer basadas en presentimientos

Todas estas modificaciones se deben realizar de manera sensata y con las herramientas disponibles para asegurarse de tratar el problema de rendimiento.

Cuando se abordan los problemas de rendimiento, debes seguir este enfoque científico:

  1. Establecer el rendimiento inicial con una medición
  2. Observar la causa del problema
  3. Modificar el código según las observaciones
  4. Medir el rendimiento y compararlo con el valor inicial
  5. Repetir

Si no sigues ningún método estructurado, algunos de los cambios podrían mejorar el rendimiento, pero otros podrían disminuirlo, y podrías obtener el mismo resultado.

Te recomendamos ver el siguiente video para mejorar el rendimiento de la app con Compose que detalla el recorrido para corregir los problemas de rendimiento, además de ofrecer algunas sugerencias para mejorarlo.

Cómo generar perfiles de Baseline

Antes de comenzar a investigar los problemas de rendimiento, genera un Perfil de Baseline para tu app. En Android 6 (nivel de API 23) y versiones posteriores, las apps ejecutan código que se interpreta en el tiempo de ejecución y se compila justo a tiempo (JIT) y con anticipación (AOT) en el momento de la instalación. El código interpretado y compilado de forma JIT se ejecuta más lentamente que cuando se compila de forma AOT, pero ocupa menos espacio en el disco y en la memoria, y es por eso que la compilación AOT no debería aplicarse a todo el código.

Cuando implementas perfiles de Baseline, puedes mejorar el inicio de la app en un 30% y reducir la ejecución de código en modo JIT en el tiempo de ejecución en ocho veces, como se muestra en la siguiente imagen basada en la app de ejemplo Now in Android:

b51455a2ca65ea8.png

Para obtener más información sobre los perfiles de Baseline, consulta los siguientes recursos:

Cómo medir el rendimiento

Para medir el rendimiento, recomendamos configurar y escribir comparativas con Jetpack Macrobenchmark. Las comparativas de Macrobenchmark son pruebas de instrumentación que interaccionan con la app como lo haría un usuario, a la vez que se supervisa su rendimiento. Esto significa que no contaminan el código de la app con código de pruebas y, por lo tanto, proporcionan información de rendimiento confiable.

En este codelab, ya configuramos la base de código y escribimos las comparativas para enfocarnos directamente en corregir los problemas de rendimiento. Si no sabes con seguridad cómo configurar y usar Macrobenchmark en tu proyecto, consulta los siguientes recursos:

Con las comparativas de Macrobenchmark, puedes elegir uno de los siguientes modos de compilación:

  • None: Restablece el estado de la compilación y ejecuta todo en modo JIT.
  • Partial: Compila previamente la app con perfiles de Baseline o iteraciones de calentamiento, y ejecuta en modo JIT.
  • Full: Compila previamente todo el código de la app, de modo que no haya código en ejecución en el modo JIT.

En este codelab, únicamente usarás el modo CompilationMode.Full() para las comparativas porque solo te ocuparás de los cambios que hagas en el código, no del estado de compilación de la app. Este enfoque te permite reducir la varianza que se originaría a partir del código en ejecución en modo JIT, lo que se reduciría al implementar perfiles de Baseline personalizados. Ten en cuenta que el modo Full puede tener un efecto negativo en el inicio de la app, así que no lo uses para las comparativas que miden el inicio de la app, sino que únicamente para las que miden las mejoras de rendimiento del tiempo de ejecución.

Una vez que hayas completado las mejoras de rendimiento y quieras verificar la ejecución cuando los usuarios instalan la app, usa el modo CompilationMode.Partial() que usa los perfiles de Baseline.

En la siguiente sección, aprenderás cómo leer los registros para detectar los problemas de rendimiento.

4. Analiza el rendimiento con el registro del sistema

Con una compilación debuggable de la app, puedes usar el Inspector de diseño con el recuento de composiciones para comprender rápidamente cuándo algo se recompone con demasiada frecuencia.

b7edfea340674732.gif

Sin embargo, se trata solo de una parte de la investigación general sobre el rendimiento porque solo obtienes mediciones de proxy y no el tiempo real que implicó la renderización de esos elementos componibles. Probablemente no tenga importancia si algo se recompone N veces, si la duración total es de menos de un milisegundo. Pero, por otro lado, es importante si algo se compone solo una vez o dos, y hacerlo le lleva 100 milisegundos. Con frecuencia, un elemento componible podría componerse solo una vez y, aun así, requerir demasiado tiempo para hacerlo y ralentizar la pantalla.

Para investigar los problemas de rendimiento de manera confiable y obtener información valiosa sobre lo que hace la app y si le lleva más tiempo del que debería, puedes usar el registro del sistema con registro de composición.

El registro del sistema te brinda información sobre el tiempo de todos los procesos que suceden en la app. No le agrega una sobrecarga y, por ende, puedes mantenerlo en la app de productividad sin tener que preocuparte por efectos negativos en el rendimiento.

Cómo configurar el seguimiento de composición

La función de composición propaga de forma automática información en sus fases de tiempo de ejecución, como cuando un elemento se recompone o cuando un diseño diferido carga previamente elementos. Sin embargo, la información no es suficiente para determinar con efectividad cuál podría ser la sección problemática. Para mejorar la cantidad de información, puedes configurar el registro de composición, que te indica el nombre de cada elemento componible que se compuso durante el registro. Esto te permite comenzar a investigar los problemas de rendimiento sin tener que agregar varias secciones trace("label") personalizadas.

Para habilitar el registro de composición, sigue estos pasos:

  1. Agrega la dependencia runtime-tracing a tu módulo :app:
implementation("androidx.compose.runtime:runtime-tracing:1.0.0-beta01")

En este punto, podrías llevar a cabo un registro del sistema con el generador de perfiles de Android Studio que incluyera toda la información, pero usaremos las comparativas de Macrobenchmark para las mediciones del rendimiento y el registro del sistema.

  1. Agrega dependencias adicionales al módulo :measure para habilitar el registro de composición con Macrobenchmark:
implementation("androidx.tracing:tracing-perfetto:1.0.0")
implementation("androidx.tracing:tracing-perfetto-binary:1.0.0")
  1. Agrega el aumento de instrumentación androidx.benchmark.fullTracing.enable=true al archivo build.gradle del módulo :measure:
defaultConfig {
    // ...
    testInstrumentationRunnerArguments["androidx.benchmark.fullTracing.enable"] = "true"
}

Para saber cómo configurar un registro de composición para, por ejemplo, usarlo desde la terminal, consulta la documentación.

Cómo capturar el rendimiento inicial con Macrobenchmark

Existen varias formas de recuperar un archivo del registro del sistema. Por ejemplo, podrías registrarte en el generador de perfiles de Android Studio, capturarlo en el dispositivo o recuperar un registro del sistema registrado con Macrobenchmark. En este codelab, usarás los registros de la biblioteca de Macrobenchmark.

Este proyecto contiene comparativas en el módulo :measure que puedes ejecutar para tomar las mediciones del rendimiento. En este proyecto, las comparativas están configuradas para ejecutar solo una iteración con el objetivo de ahorrar tiempo durante este codelab. En la app real, si la varianza resultante es alta, se recomienda tener al menos diez iteraciones.

Para capturar el rendimiento inicial, usa la prueba AccelerateHeavyScreenBenchmark que desplaza la pantalla de la primera tarea, sigue estos pasos:

  1. Abre el archivo AccelerateHeavyScreenBenchmark.kt.
  2. Ejecuta la comparativa con la acción del margen junto a la clase de comparativas:

e93fb1dc8a9edf4b.png

Esta comparativa desplaza la pantalla Task 1 y captura la latencia de fotogramas y las secciones de registro

personalizadas.

8afabbbbbfc1d506.gif

Una vez que se complete la comparativa, deberías ver los resultados en el panel de resultados de Android Studio:

AccelerateHeavyScreenBenchmark_accelerateHeavyScreenCompilationFull
ImagePlaceholderCount               min  20.0,   median  20.0,   max  20.0
ImagePlaceholderMs                  min  22.9,   median  22.9,   max  22.9
ItemTagCount                        min  80.0,   median  80.0,   max  80.0
ItemTagMs                           min   3.2,   median   3.2,   max   3.2
PublishDate.registerReceiverCount   min   1.0,   median   1.0,   max   1.0
PublishDate.registerReceiverMs      min   1.9,   median   1.9,   max   1.9
frameDurationCpuMs                  P50    5.4,   P90    9.0,   P95   10.5,   P99   57.5
frameOverrunMs                      P50   -4.2,   P90   -3.5,   P95   -3.2,   P99   74.9
Traces: Iteration 0

Las métricas importantes en el resultado son las siguientes:

  • frameDurationCpuMs: Indica el tiempo que llevó renderizar fotogramas. Cuanto más breve, mejor.
  • frameOverrunMs: Indica el tiempo excedente del límite para fotogramas, incluido el funcionamiento en la GPU. Un número negativo es un buen resultado porque implica que sobró tiempo.

Las otras métricas, como la métrica ImagePlaceholderMs, usan secciones de registro personalizadas y calculan la duración total de todas esas secciones en el archivo de registro, además de la cantidad de veces que se produjo con la métrica ImagePlaceholderCount.

Todas estas métricas pueden ayudarnos a comprender si los cambios que hacemos en nuestra base de código mejoran el rendimiento.

Cómo leer el archivo de registro

Puedes leer el archivo de registro desde Android Studio o con Perfetto, la herramienta basada en la Web.

Si bien el generador de perfiles de Android Studio es una buena manera de abrir rápidamente un registro y mostrar el proceso de la app, Perfetto proporciona capacidades de investigación más detalladas para todos los procesos que se ejecutan en un sistema con consultas en SQL eficaces y mucho más. En este codelab, usarás Perfetto para analizar registros del sistema.

  1. Abre el sitio web de Perfetto, que carga el panel de la herramienta.
  2. Ubica los registros del sistema que capturó Macrobenchmark en el sistema de archivos del hosting, que se guardan en la carpeta [module]/outputs/connected_android_test_additional_output/benchmarkRelease/connected/[device]/. Cada iteración de comparativa procesa un archivo de registro separado, y cada archivo contiene las mismas interacciones con la app.

51589f24d9da28be.png

  1. Arrastra el archivo AccelerateHeavyScreenBenchmark_...iter000...perfetto-trace a la IU de Perfetto y espera hasta que cargue el archivo de registro.
  2. Opcional: Si no puedes ejecutar la comparativa y generar el archivo de registro, descarga el nuestro y arrástralo a Perfetto:

547507cdf63ae73.gif

  1. Busca el proceso com.compose.performance en tu app. Por lo general, la app en primer plano está por debajo de los carriles de información del hardware y de un par de carriles del sistema.
  2. Abre el menú desplegable con el nombre del proceso de la app. Verás la lista de subprocesos que se ejecutan en la app. Mantén el archivo de registro abierto porque lo necesitarás en el paso siguiente.

582b71388fa7e8b.gif

Si quieres buscar un problema de rendimiento en la app, puedes aprovechar los cronogramas esperado y real en la parte superior de la lista de subprocesos de la app:

1bd6170d6642427e.png

El cronograma esperado indica cuándo el sistema espera que la app genere los fotogramas para mostrar UI fluida y de cumplimiento que, en este caso, es de 16 ms y 600 µs (1000 ms/60). El cronograma real muestra la duración real de los fotogramas que genera la app, incluido el funcionamiento de la GPU.

Es probable que veas diferentes colores, y esto indica lo siguiente:

  • Fotograma verde: El fotograma se produjo puntualmente.
  • Fotograma rojo: El fotograma con bloqueos llevó más tiempo de lo esperado. Deberías investigar el trabajo realizado en estos fotogramas para evitar problemas de rendimiento.
  • Fotograma verde claro: El fotograma se produjo conforme el plazo, pero se presentó tardíamente, lo que generó una latencia de entrada aumentada.
  • Fotograma amarillo: El fotograma se bloqueó, pero la app no fue el motivo.

Cuando la IU se renderiza en la pantalla, los cambios deben ser más rápidos que el tiempo de creación de un fotograma que espera el dispositivo. Históricamente, esto fue de casi 16.6 ms dado que la frecuencia de actualización de la pantalla fue de 60 Hz. Sin embargo, para los dispositivos Android modernos, puede ser de casi 11 ms o menos porque la frecuencia de actualización de la pantalla es de 90 Hz o más rápida. También puede ser diferente para cada fotograma debido a frecuencias de actualización variables.

Por ejemplo, si tu IU se compone de 16 elementos, entonces cada elemento se debe crear en apenas 1 ms para evitar que se omitan fotogramas. Por otro lado, si solo tienes un elemento, como un reproductor de video, la composición sin bloqueos puede llevar hasta 16 ms.

Cómo comprender el gráfico de llamadas del registro del sistema

En la siguiente imagen, verás un ejemplo de una versión simplificada de un registro del sistema que muestra una recomposición.

8f16db803ca19a7d.png

Cada barra, de un extremo a otro, muestra en la parte inferior el tiempo total. Las barras también corresponden a las secciones de código de las funciones llamadas. Las llamadas de composición se recomponen en la jerarquía de composición. El primer elemento componible es MaterialTheme. Dentro de MaterialTheme hay datos locales de composición que proporcionan información sobre asociación de temas. Desde allí, se llama al elemento HomeScreen componible. El elemento componible de la pantalla principal llama a los elementos MyImage y MyButton componibles como parte de su composición.

Las brechas en los registros del sistema provienen de código sin registrar que se ejecuta porque los registros del sistema solo muestran código que se marca para el registro. El código que se ejecuta se procesa después de la llamada de MyImage pero antes de la llamada de MyButton, y lleva una cantidad de tiempo equivalente al tamaño de la brecha.

En el paso siguiente, analizarás el registro que tomaste en el paso anterior.

5. Acelera elementos componibles pesados

Como primera tarea cuando intentas optimizar el rendimiento de la app, deberías buscar elementos componibles pesados o una tarea de ejecución prolongada en el subproceso principal. El trabajo de ejecución prolongada podría tener diferentes significados, según qué tan complicada es tu IU y cuánto tiempo tienes para componerla.

Entonces, si se omite un fotograma, tienes que buscar los elementos componibles que tardan demasiado tiempo en procesarse y acelerarlos. Para ello, debes descargar el subproceso principal, o bien omitir parte del trabajo que estos elementos hacen en el subproceso principal.

Para analizar el registro tomado de la prueba AccelerateHeavyScreenBenchmark, sigue estos pasos:

  1. Abre el registro del sistema que tomaste en el paso anterior.
  2. Acerca el primer fotograma largo, que contiene la inicialización de IU después de la carga de los datos. El contenido del fotograma es similar a la siguiente imagen:

838787b87b14bbaf.png

En el registro, puedes ver que suceden muchas cosas dentro de un fotograma, que se puede encontrar en la sección Choreographer#doFrame. Puedes ver en la imagen que la mayor parte del trabajo proviene del elemento componible que contiene la sección ImagePlaceholder, que carga una imagen grande.

Cómo evitar cargar imágenes grandes en el subproceso principal

Es probable que resulte evidente cargar imágenes de manera asíncrona de una red que usa una de las bibliotecas convenientes, como Coil o Glide, pero, ¿qué sucede si tienes que mostrar una imagen grande que tienes localmente en la app?

La función de componibilidad común painterResource que carga una imagen de los recursos carga la imagen en el subproceso principal durante la composición. Esto significa que si la imagen es grande, puede bloquear el subproceso principal con algo de trabajo.

En tu caso, puedes ver el problema como parte del marcador de posición de la imagen asíncrona. La función de componibilidad painterResource carga una imagen de marcador de posición que demora unos 23 ms en cargar.

c83d22c3870655a7.jpeg

Existen varias formas de mejorar este problema, incluidas las siguientes opciones:

  • Carga la imagen de manera asíncrona.
  • Achica la imagen de modo que se cargue más rápidamente.
  • Usa un elemento de diseño vectorial que se escale según el tamaño requerido.

Para corregir este problema de rendimiento, sigue estos pasos:

  1. Navega al archivo AccelerateHeavyScreen.kt.
  2. Ubica la función de componibilidad imagePlaceholder()que carga la imagen. La imagen del marcador de posición tiene dimensiones de 1,600 x 1,600 px, que es claramente demasiado grande para lo que muestra.

53b34f358f2ff74.jpeg

  1. Cambia el elemento de diseño a R.drawable.placeholder_vector:
@Composable
fun imagePlaceholder() =
    trace("ImagePlaceholder") { painterResource(R.drawable.placeholder_vector) }
  1. Vuelve a ejecutar la prueba AccelerateHeavyScreenBenchmark, que recompila la app y vuelve a tomar el registro del sistema.
  2. Arrastra el registro del sistema al panel de Perfetto.

Como alternativa, puedes descargar el registro:

  1. Busca la sección del registro ImagePlaceholder, que te muestra directamente la parte mejorada.

abac4ae93d599864.png

  1. Observa que la función ImagePlaceholder ya no bloquea tanto el subproceso principal.

8e76941fca0ae63c.jpeg

Como una solución alternativa en la app real, es probable que una imagen del marcador de posición no sea lo que genera complicaciones, pero que material gráfico sí lo haga. En este caso, podrías usar la función de componibilidad rememberAsyncImage de Coil, que carga el elemento componible de manera asíncrona. Esta solución mostrará un espacio vacío hasta que se cargue el marcador de posición, así que ten en cuenta que podrías necesitar tener un marcador de posición para estos tipos de imágenes.

Aún hay otros aspectos que no se ejecutan bien, de los que te ocuparás en el paso siguiente.

6. Descarga una operación pesada en un subproceso en segundo plano

Si seguimos investigando el mismo elemento en busca de problemas adicionales, verás que hay secciones con el nombre binder transaction a los que la descarga le lleva a cada uno aproximadamente 1 ms.

5c08376b3824f33a.png

Las secciones con el nombre binder transaction muestran que hubo una comunicación entre tu proceso y el proceso del sistema. Se trata de una forma normal de recuperar información del sistema, como recuperar un servicio del sistema.

Estas transacciones se incluyen en muchas de las APIs que se comunican con el sistema. Por ejemplo, cuando se recupera un servicio del sistema con getSystemService, se registra un receptor de transmisiones o se solicita un ConnectivityManager.

Lamentablemente, estas transacciones no proporcionan mucha información sobre lo que solicitan, así que tienes que analizar el código según los usos de API mencionados y, luego, agregar una sección trace personalizada para asegurarte de que se trata de la parte problemática.

Para mejorar las transacciones de Binder, sigue estos pasos:

  1. Abre el archivo AccelerateHeavyScreen.kt.
  2. Ubica el elemento PublishedText componible. Este elemento formatea una fecha y una hora según la zona horaria actual, y registra un objeto BroadcastReceiver que mantiene un registro de cambios de zona horaria. Contiene una variable de estado currentTimeZone con la zona horaria predeterminada del sistema como valor inicial y, luego, un DisposableEffect que registra un receptor de transmisiones para los cambios de zona horaria. Por último, este elemento componible muestra una fecha y hora formateada con Text. DisposableEffect, que es una buena opción en esta situación porque necesitas contar con una manera de cancelar el registro del receptor de transmisiones, que se hace en la expresión lambda onDispose. La parte problemática, sin embargo, es que el código dentro de DisposableEffect bloquea el subproceso principal:
@Composable
fun PublishedText(published: Instant, modifier: Modifier = Modifier) {
    val context = LocalContext.current
    var currentTimeZone: TimeZone by remember { mutableStateOf(TimeZone.currentSystemDefault()) }

    DisposableEffect(Unit) {
        val receiver = object : BroadcastReceiver() {
            override fun onReceive(context: Context?, intent: Intent?) {
                currentTimeZone = TimeZone.currentSystemDefault()
            }
        }

        // TODO Codelab task: Wrap with a custom trace section
        context.registerReceiver(receiver, IntentFilter(Intent.ACTION_TIMEZONE_CHANGED))

        onDispose { context.unregisterReceiver(receiver) }
    }

    Text(
        text = published.format(currentTimeZone),
        style = MaterialTheme.typography.labelMedium,
        modifier = modifier
    )
}
  1. Une el context.registerReceiver con una llamada trace para asegurarte de que es esto lo que efectivamente causa todo el binder transactions:
trace("PublishDate.registerReceiver") {
    context.registerReceiver(receiver, IntentFilter(Intent.ACTION_TIMEZONE_CHANGED))
}

En general, un código que se ejecuta durante tanto tiempo en el subproceso principal podría no causar muchos inconvenientes, pero el hecho de que esta transacción se ejecute para cada elemento visible en la pantalla sí podría ser la causa de problemas. Si se contempla que hay seis elementos visibles en la pantalla, estos se tienen que componer con el primer fotograma. Estas llamadas por sí solas pueden llevar 12 ms, que es casi la totalidad del tiempo límite para un fotograma.

Para corregir esto, tienes que transferir el registro de transmisión en un subproceso diferente. Puedes hacerlo con corrutinas.

  1. Obtén un alcance que esté vinculado al ciclo de vida del elemento componible val scope = rememberCoroutineScope().
  2. Dentro del efecto, lanza una corrutina en un despachador que no sea Dispatchers.Main. Por ejemplo, en este caso, Dispatchers.IO. De este modo, el registro de transmisión no bloquea el subproceso principal, pero el estado real currentTimeZone se mantiene en el subproceso principal.
val scope = rememberCoroutineScope()

DisposableEffect(Unit) {
    val receiver = object : BroadcastReceiver() {
        override fun onReceive(context: Context?, intent: Intent?) {
            currentTimeZone = TimeZone.currentSystemDefault()
        }
    }

    // launch the coroutine on Dispatchers.IO
    scope.launch(Dispatchers.IO) {
        trace("PublishDate.registerReceiver") {
            context.registerReceiver(receiver, IntentFilter(Intent.ACTION_TIMEZONE_CHANGED))
        }
    }

    onDispose { context.unregisterReceiver(receiver) }
}

Hay un paso más para la optimización. No necesitas un receptor de transmisiones para cada elemento de la lista, sino que necesitas uno solo y deberías elevarlo.

Puedes elevarlo y pasar el parámetro de zona horaria al árbol de elementos componibles o, dado que no se usa en muchas partes en tu IU, puedes usar datos locales de composición.

Para los fines de este codelab, conserva el receptor de transmisiones como parte del árbol de elementos componibles. Sin embargo, en la app real, podría ser beneficioso separarlo en una capa de datos para evitar contaminar el código de la IU.

  1. Define los datos locales de composición con la zona horaria predeterminada del sistema:
val LocalTimeZone = compositionLocalOf { TimeZone.currentSystemDefault() }
  1. Actualiza el elemento ProvideCurrentTimeZone componible que lleva una expresión lambda content para proporcionar la zona horaria actual:
@Composable
fun ProvideCurrentTimeZone(content: @Composable () -> Unit) {
    var currentTimeZone = TODO()

    CompositionLocalProvider(
        value = LocalTimeZone provides currentTimeZone,
        content = content,
    )
}
  1. Quita DisposableEffect del elemento PublishedText componible y colócalo en el nuevo para elevarlo ahí, y reemplaza el currentTimeZone con el estado y el efecto secundario:
@Composable
fun ProvideCurrentTimeZone(content: @Composable () -> Unit) {
    val context = LocalContext.current
    val scope = rememberCoroutineScope()
    var currentTimeZone: TimeZone by remember { mutableStateOf(TimeZone.currentSystemDefault()) }

    DisposableEffect(Unit) {
        val receiver = object : BroadcastReceiver() {
            override fun onReceive(context: Context?, intent: Intent?) {
                currentTimeZone = TimeZone.currentSystemDefault()
            }
        }

        scope.launch(Dispatchers.IO) {
            trace("PublishDate.registerReceiver") {
                context.registerReceiver(receiver, IntentFilter(Intent.ACTION_TIMEZONE_CHANGED))
            }
        }

        onDispose { context.unregisterReceiver(receiver) }
    }

    CompositionLocalProvider(
        value = LocalTimeZone provides currentTimeZone,
        content = content,
    )
}
  1. Une un elemento componible en el que desees que datos locales de composición sean válidos con el ProvideCurrentTimeZone. Puedes unir el AccelerateHeavyScreen entero, según se muestra en el siguiente fragmento:
@Composable
fun AccelerateHeavyScreen(items: List<HeavyItem>, modifier: Modifier = Modifier) {
    // TODO: Codelab task: Wrap this with timezone provider
    ProvideCurrentTimeZone {
        Box(
            modifier = modifier
                .fillMaxSize()
                .padding(24.dp)
        ) {
            ScreenContent(items = items)

            if (items.isEmpty()) {
                CircularProgressIndicator(modifier = Modifier.align(Alignment.Center))
            }
        }
    }
}
  1. Cambia el elemento PublishedText componible para que solo contenga la funcionalidad de formateo básico y lea el valor actual de los datos locales de composición a través de LocalTimeZone.current:
@Composable
fun PublishedText(published: Instant, modifier: Modifier = Modifier) {
    Text(
        text = published.format(LocalTimeZone.current),
        style = MaterialTheme.typography.labelMedium,
        modifier = modifier
    )
}
  1. Vuelve a ejecutar la comparativa, que compila la app.

Como alternativa, puedes descargar el registro del sistema con el código corregido:

  1. Arrastra el archivo de registro al panel de Perfetto. Todas las secciones del binder transactions desaparecieron del subproceso principal.
  2. Busca el nombre de la sección que es similar al paso anterior. Puedes buscarlo en uno de los otros subprocesos que crearon las corrutinas (DefaultDispatch):

87feee260f900a76.png

7. Quita las subcomposiciones innecesarias

Quitaste el código pesado del subproceso principal, así que eso ya no bloquea la composición. Todavía hay aspectos por mejorar. Puedes quitar la sobrecarga innecesaria en forma de un elemento LazyRow componible en cada elemento.

En el ejemplo, cada uno de los elementos contiene una fila de rastreadores, según se destaca en la siguiente imagen:

e821c86604d3e670.png

Esta fila está implementada con un elemento LazyRow componible porque es sencillo programarlo de esta manera. Pasa los elementos al elemento LazyRow componible, y este se ocupará del resto:

@Composable
fun ItemTags(tags: List<String>, modifier: Modifier = Modifier) {
    // TODO: remove unnecessary lazy layout
    LazyRow(
        modifier = modifier
            .padding(4.dp)
            .fillMaxWidth(),
        horizontalArrangement = Arrangement.spacedBy(2.dp)
    ) {
        items(tags) { ItemTag(it) }
    }
}

El problema es que, mientras los diseños de Lazy se destacan entre diseños donde tienes muchos más elementos que el tamaño restringido, estos generan costos adicionales, algo que es innecesario cuando no se requiere la composición diferida.

Dada la naturaleza de los elementos Lazy componibles, que usan un elemento SubcomposeLayout componible, siempre se muestran como varios fragmentos de trabajo; primero, el contenedor y, luego, los elementos que se pueden ver en la pantalla en ese momento, que es el segundo fragmento de trabajo. También puedes buscar un registro compose:lazylist:prefetch en el registro del sistema, que indica que elementos adicionales se incorporan en el viewport y, por lo tanto, se cargan previamente para estar listos con anticipación.

b3dc3662b5885a2e.jpeg

Para determinar de forma aproximada el tiempo que lleva en tu caso, abre el mismo archivo de registro. Puedes ver que hay secciones desconectadas del elemento principal. Cada elemento consta del elemento real que se compone y de los elementos rastreadores. De esta manera, cada elemento se compone en unos 2.5 ms, y si multiplicas este tiempo por la cantidad de elementos visibles, se obtiene otra buena parte de trabajo.

a204721c80497e0f.jpeg

Para corregir esto, sigue estos pasos:

  1. Navega al archivo AccelerateHeavyScreen.kt y ubica el elemento ItemTags componible.
  2. Cambia la implementación LazyRow por un elemento Row componible que se itere en la lista tags, como se muestra en el siguiente fragmento:
@Composable
fun ItemTags(tags: List<String>, modifier: Modifier = Modifier) {
    Row(
        modifier = modifier
            .padding(4.dp)
            .fillMaxWidth()
        horizontalArrangement = Arrangement.spacedBy(2.dp)
    ) {
        tags.forEach { ItemTag(it) }
    }
}
  1. Vuelve a ejecutar la comparativa, que también compilará la app.
  2. Opcional: Descarga el registro del sistema con el código corregido:

  1. Busca las secciones de ItemTag, observa que lleva menos tiempo y usa la misma sección raíz de Compose:recompose.

219cd2e961defd1.jpeg

Una situación similar podría ocurrir con otros contenedores que usan un elemento SubcomposeLayout componible, por ejemplo, un elemento BoxWithConstraints componible. Puede abarcar la creación de los elementos en las secciones de Compose:recompose, que podría no mostrarse directamente como un fotograma bloqueado, pero que puede ser visible para el usuario. Si puedes, intenta evitar un elemento BoxWithConstraints componible en cada elemento, ya que solo podría ser necesario cuando compones una IU diferente según el espacio disponible.

En esta sección, aprendiste a corregir composiciones que llevan demasiado tiempo.

8. Compara los resultados con el análisis comparativo inicial

Ahora que terminaste de optimizar el rendimiento de la pantalla, deberías comparar los resultados de la comparación con el análisis inicial.

  1. Abre Test History en el panel de ejecución de Android Studio 667294bf641c8fc2.png
  2. Selecciona la ejecución más antigua que se relaciona con la comparativa inicial sin ningún cambio y compara las métricas de frameDurationCpuMs y frameOverrunMs. Deberías ver resultados similares a los de la siguiente tabla:

Antes

AccelerateHeavyScreenBenchmark_accelerateHeavyScreenCompilationFull
ImagePlaceholderCount               min  20.0,   median  20.0,   max  20.0
ImagePlaceholderMs                  min  22.9,   median  22.9,   max  22.9
ItemTagCount                        min  80.0,   median  80.0,   max  80.0
ItemTagMs                           min   3.2,   median   3.2,   max   3.2
PublishDate.registerReceiverCount   min   1.0,   median   1.0,   max   1.0
PublishDate.registerReceiverMs      min   1.9,   median   1.9,   max   1.9
frameDurationCpuMs                  P50    5.4,   P90    9.0,   P95   10.5,   P99   57.5
frameOverrunMs                      P50   -4.2,   P90   -3.5,   P95   -3.2,   P99   74.9
Traces: Iteration 0
  1. Selecciona la ejecución más reciente que se relaciona con la comparativa con todas las optimizaciones. Deberías ver resultados similares a los de la siguiente tabla:

Después

AccelerateHeavyScreenBenchmark_accelerateHeavyScreenCompilationFull
ImagePlaceholderCount               min  20.0,   median  20.0,   max  20.0
ImagePlaceholderMs                  min   2.9,   median   2.9,   max   2.9
ItemTagCount                        min  80.0,   median  80.0,   max  80.0
ItemTagMs                           min   3.4,   median   3.4,   max   3.4
PublishDate.registerReceiverCount   min   1.0,   median   1.0,   max   1.0
PublishDate.registerReceiverMs      min   1.1,   median   1.1,   max   1.1
frameDurationCpuMs                  P50    4.3,   P90    7.7,   P95    8.8,   P99   33.1
frameOverrunMs                      P50  -11.4,   P90   -8.3,   P95   -7.3,   P99   41.8
Traces: Iteration 0

Si verificas especialmente la fila frameOverrunMs, puedes ver todos los percentiles mejorados:

P50

P90

P95

P99

antes

-4.2

-3.5

-3.2

74.9

después

-11.4

-8.3

-7.3

41.8

mejora

171%

137%

128%

44%

En la sección siguiente, aprenderás a corregir una composición que se da con demasiada frecuencia.

9. Evita recomposiciones innecesarias

La composición tiene las siguientes 3 fases:

  • La composición determina lo que se mostrará al compilar un árbol de elementos componibles.
  • El diseño toma ese árbol y determina dónde aparecerán los elementos componibles en la pantalla.
  • El dibujo plasma los elementos componibles en la pantalla.

Por lo general, el orden de estas fases es el mismo, y esto permite que los datos fluyan en una dirección desde la composición hasta el diseño y el dibujo, hasta producir un fotograma de IU.

2147ae29192a1556.png

BoxWithConstraints, los diseños diferidos (por ejemplo, LazyColumn o LazyVerticalGrid) y todos los diseños basados en SubcomposeLayout elementos componibles son excepciones notables, en las que la composición de los elementos secundarios depende de las fases de diseño de los elementos superiores.

Por lo general, la composición es la fase más costosa de ejecutar, ya que es la fase que implica la mayor cantidad de trabajo, y también puede que origines que otros elementos componibles no relacionados se recompongan.

La mayoría de los fotogramas contiene las tres fases, pero la de composición puede efectivamente omitir una fase por completo si no hay trabajo que hacer. Para incrementar el rendimiento de tu app, puedes aprovechar esta capacidad.

Cómo aplazar fases de composición con modificadores lambda

Las funciones de componibilidad se ejecutan en la fase de composición. Para permitir que el código se ejecute en otro momento, puedes proporcionarlo como una función lambda.

Para hacerlo, sigue estos pasos:

  1. Abre el archivo PhasesComposeLogo.kt.
  2. Navega a la pantalla Task 2 de la app. Verás un logotipo que rebota en el borde de la pantalla.
  3. Abre el Inspector de diseño y, luego, inspecciona los recuentos de recomposición. Verás una cantidad de recomposiciones de rápido crecimiento.

a9e52e8ccf0d31c1.png

  1. Opcional: Ubica el archivo PhasesComposeLogoBenchmark.kt y ejecútalo para recuperar el registro del sistema y ver la composición de la sección del registro PhasesComposeLogo que se produce en cada fotograma. Las recomposiciones se muestran en un registro como secciones que se repiten con el mismo nombre.

4b6e72578c89b2c1.jpeg 7036a895a31138d3.png

  1. Si es necesario, cierra el generador de perfiles del Inspector de diseño y, luego, regresa al código. Verás el elemento PhaseComposeLogo componible que se parece a lo siguiente:
@Composable
fun PhasesComposeLogo() = trace("PhasesComposeLogo") {
    val logo = painterResource(id = R.drawable.compose_logo)
    var size by remember { mutableStateOf(IntSize.Zero) }
    val logoPosition by logoPosition(size = size, logoSize = logo.intrinsicSize)

    Box(
        modifier = Modifier
            .fillMaxSize()
            .onPlaced {
                size = it.size
            }
    ) {
        with(LocalDensity.current) {
            Image(
                painter = logo,
                contentDescription = "logo",
                modifier = Modifier.offset(logoPosition.x.toDp(), logoPosition.y.toDp())
            )
        }
    }
}

El elemento logoPosition componible contiene una lógica que cambia su estado con cada fotograma y se parece a lo siguiente:

@Composable
fun logoPosition(size: IntSize, logoSize: Size): State<IntOffset> =
    produceState(initialValue = IntOffset.Zero, size, logoSize) {
        if (size == IntSize.Zero) {
            this.value = IntOffset.Zero
            return@produceState
        }

        var xDirection = 1
        var yDirection = 1

        while (true) {
            withFrameMillis {
                value += IntOffset(x = MOVE_SPEED * xDirection, y = MOVE_SPEED * yDirection)

                if (value.x <= 0 || value.x >= size.width - logoSize.width) {
                    xDirection *= -1
                }

                if (value.y <= 0 || value.y >= size.height - logoSize.height) {
                    yDirection *= -1
                }
            }
        }
    }

El estado se lee en el elemento PhasesComposeLogo componible con el modificador Modifier.offset(x.dp, y.dp), que significa que lo lee en la composición.

Este modificador es el motivo por el que la app se recompone en cada fotograma de esta animación. En este caso, hay una alternativa sencilla: el modificador Offset basado en lambda.

  1. Actualiza el elemento Image componible para que use el modificador Modifier.offset, que acepta una expresión lambda que devuelve un objeto IntOffset, como en el siguiente fragmento:
Image(
  painter = logo,
  contentDescription = "logo",
  modifier = Modifier.offset { IntOffset(logoPosition.x,  logoPosition.y) }
)
  1. Vuelve a ejecutar la app y verifica el inspector de diseño. Verás que la animación ya no genera ninguna recomposición.

Recuerda que no deberías tener que recomponer solo para ajustar el diseño de una pantalla, sobre todo durante el desplazamiento, lo que genera que se bloqueen fotogramas. La recomposición que se produce durante el desplazamiento casi siempre es innecesaria y se debería evitar.

Otros modificadores de lambda

El modificador Modifier.offset no es el único modificador con la versión lambda. En la siguiente tabla, verás los modificadores comunes que se recompondrían cada vez, algo que se puede reemplazar por sus alternativas aplazadas cuando pasan un valor de estado que cambia frecuentemente:

Modificador común

Alternativa aplazada

.background(color)

.drawBehind { drawRect(color) }

.offset(0.dp, y)

.offset { IntOffset(0, y.roundToPx()) }

.alpha(a).rotate(r).scale(s)

.graphicsLayer { alpha = a; rotationZ = r; scaleX = s; scaleY = s}

10. Aplaza fases de composición con diseño personalizado

El uso de un modificador basado en lambda es, a menudo, la manera más sencilla de evitar invalidar la composición, pero a veces no hay un modificador basado en lambda que haga lo que necesitas. En estos casos, puedes implementar directamente un diseño personalizado o, incluso, un elemento Canvas componible para pasar de forma directa a la fase de dibujo. Las lecturas de estado de composición completadas dentro de un diseño personalizado solo invalidan el diseño y omiten la recomposición. Como lineamiento general, si solo quieres ajustar el diseño o el tamaño, pero no quieres agregar o quitar elementos componibles, en algunos casos, puedes lograr el efecto sin invalidar la composición en absoluto.

Para ello, sigue estos pasos:

  1. Abre el archivo PhasesAnimatedShape.kt y, luego, ejecuta la app.
  2. Navega a la pantalla Task 3. Esta pantalla contiene una forma que cambia de tamaño cuando haces clic en un botón. El valor de tamaño se anima con la API de animación de Compose animateDpAsState.

51dc23231ebd5f1a.gif

  1. Abre el Inspector de diseño.
  2. Haz clic en Toggle size.
  3. Observa que la forma se recompone en cada fotograma de la animación.

63d597a98fca1133.png

El elemento MyShape componible toma el objeto size como un parámetro, que es una lectura del estado. Esto significa que cuando el objeto size cambia, el elemento PhasesAnimatedShape componible (el alcance de recomposición más cercano) se recompone y, subsecuentemente, el elemento MyShape componible también se recompone porque sus entradas cambiaron.

Para omitir la recomposición, sigue estos pasos:

  1. Cambia el parámetro size a una función lambda para que los cambios de tamaño no recompongan directamente el elemento MyShape componible:
@Composable
fun MyShape(
    size: () -> Dp,
    modifier: Modifier = Modifier
) {
  // ...
  1. Actualiza el sitio de llamada en el elemento PhasesAnimatedShape componible para usar la función lambda:
MyShape(size = { size }, modifier = Modifier.align(Alignment.Center))

Cambiar el parámetro size a una expresión lambda demora la lectura del estado. Ahora, eso ocurre cuando se invoca la función lambda.

  1. Cambia el cuerpo del elemento MyShape componible por la siguiente expresión:
Box(
    modifier = modifier
        .background(color = Purple80, shape = CircleShape)
        .layout { measurable, _ ->
            val sizePx = size()
                .roundToPx()
                .coerceAtLeast(0)

            val constraints = Constraints.fixed(
                width = sizePx,
                height = sizePx,
            )

            val placeable = measurable.measure(constraints)
            layout(sizePx, sizePx) {
                placeable.place(0, 0)
            }
        }
)

En la primera línea de la expresión lambda de medida de modificador layout, se invoca la función lambda size. Esto ocurre dentro del modificador layout, de modo que solo invalida el diseño, no la composición.

  1. Vuelve a ejecutar la app, navega hasta la pantalla Task 3 y, luego, abre el Inspector de diseño.
  2. Haz clic en Toggle Size y, a continuación, observa que el tamaño de la forma se anima igual que antes, pero el elemento MyShape componible no se recompone.

11. Evita recomposiciones con clases estables

La composición genera código que puede omitir la ejecución de un elemento componible si todos los parámetros de entrada son estables y no cambiaron desde la composición anterior. Un tipo es estable si es inmutable o si es posible que el motor de Compose sepa si su valor cambió entre recomposiciones.

Si el motor de Compose no tiene la certeza de que un elemento componible es estable, lo considerará inestable y no generará la lógica de código para omitir la recomposición, y esto significa que el elemento componible se recompondrá cada vez. Esto se puede producir cuando una clase no es de un tipo primitivo y ocurre una de las situaciones siguientes:

  • Es una clase mutable. Por ejemplo, contiene una propiedad mutable.
  • Es una clase que se define en un módulo Gradle que no usa Compose. No tiene una dependencia en el compilador de Compose.
  • Se trata de una clase que contiene una propiedad inestable.

Este comportamiento puede ser indeseable en algunos casos, donde ocasiona problemas de rendimiento y se puede cambiar cuando haces lo siguiente:

  • Cómo habilitar el modo de omisión avanzada
  • Anota el parámetro con una anotación @Immutable o @Stable.
  • Agrega la clase al archivo de configuración de estabilidad.

Para obtener más ayuda sobre la estabilidad, lee la documentación.

En esta tarea, tienes una lista de elementos que se pueden agregar, quitar o verificar, y tienes que asegurarte de que no se recomponga ningún elemento si no es necesario hacerlo. Existen dos tipos de elementos que se alternan entre los que se recrean cada vez y los que no lo hacen.

Los elementos que se recrean cada vez están aquí como simulación de un caso de uso real donde los datos provienen de una base de datos local (por ejemplo, Room o sqlDelight) o una fuente de datos remota (como solicitudes a la API o entidades de Firestore), y devuelven una nueva instancia del objeto cada vez que hay un cambio.

Varios elementos componibles tienen un modificador Modifier.recomposeHighlighter() adjunto, que puedes encontrar en nuestro repositorio de GitHub. Este modificador muestra un borde siempre que se recompone un elemento componible y puede servir como solución temporaria alternativa frente al Inspector de diseño.

127f2e4a2fc1a381.gif

Cómo habilitar el modo de omisión avanzada

El compilador 1.5.4 y versiones posteriores de Jetpack Compose vienen con una opción para habilitar el modo de omisión avanzado, lo que significa que incluso los elementos componibles con parámetros inestables pueden generar código de omisión. Se espera que este modo reduzca de manera radical la cantidad de elementos componibles que no se pueden omitir en el proyecto, lo que mejorará el rendimiento sin cambios en el código.

Para los parámetros inestables, se compara la lógica de omisión respecto de la igualdad de instancias, y esto significa que el parámetro se omitirá si se pasa la misma instancia al elemento componible que en el caso anterior. Por el contrario, los parámetros estables usan la igualdad estructural (al llamar el método Object.equals()) para determinar la lógica de omisión.

Además de la lógica de omisión, el modo de omisión avanzada también recuerda automáticamente las expresiones lambda que se incluyen en una función de componibilidad. Este hecho implica que no necesitas una llamada remember para unir una función lambda, por ejemplo, una que llama un método ViewModel.

El modo de omisión avanzada se puede habilitar en un módulo Gradle.

Para hacerlo, sigue estos pasos:

  1. Abre el archivo build.gradle.kts de la app.
  2. Actualiza el bloque composeCompiler con el siguiente fragmento:
composeCompiler {
    // Not required in Kotlin 2.0 final release
    suppressKotlinVersionCompatibilityCheck = "2.0.0-RC1"

    // This settings enables strong-skipping mode for all module in this project.
    // As an effect, Compose can skip a composable even if it's unstable by comparing it's instance equality (===).
    enableExperimentalStrongSkippingMode = true
}

Esto agrega el argumento de compilador experimentalStrongSkipping al módulo Gradle.

  1. Haz clic en b8a9619d159a7d8e.png Sync project with Gradle files.
  2. Vuelve a compilar el proyecto.
  3. Abre la pantalla Task 5 y, luego, observa que los elementos que usan igualdad estructural están marcados con un ícono EQU y no se recomponen cuando interaccionas con la lista de elementos.

1de2fd2c42a1f04f.gif

Sin embargo, otros tipos de elementos aún se recomponen. Los corregirás en el paso siguiente.

Cómo corregir la estabilidad con anotaciones

Como se mencionó anteriormente, con el módulo de omisión avanzada habilitado, un elemento componible omitirá su ejecución cuando el parámetro tenga la misma instancia que en la composición anterior. Sin embargo, este esquema no se aplica en situaciones en las que se proporciona una nueva instancia de la clase inestable con cada cambio.

En tu caso, la clase StabilityItem es inestable porque contiene una propiedad LocalDateTime inestable.

Para corregir la estabilidad de esta clase, sigue estos pasos:

  1. Navega al archivo StabilityViewModel.kt.
  2. Ubica la clase StabilityItem y agrégale la anotación @Immutable:
// TODO Codelab task: make this class Stable
@Immutable
data class StabilityItem(
    val id: Int,
    val type: StabilityItemType,
    val name: String,
    val checked: Boolean,
    val created: LocalDateTime
)
  1. Vuelve a compilar la app.
  2. Navega a la pantalla Task 5 y observa que no se recomponga ninguno de los elementos de la lista.

938aad77b78f7590.gif

Esta clase ahora usa la igualdad estructural para verificar si hubo cambios desde la composición anterior y, por ende, no hay recomposición.

Aún está el elemento componible que refiere a la fecha del cambio más reciente, que continúa con la recomposición, independientemente de lo que hayas hecho hasta ahora.

Cómo corregir la estabilidad con el archivo de configuración

El enfoque anterior funciona bien para las clases que forman parte de la base de código. Sin embargo, las clases que están fuera de tu alcance, como las clases de bibliotecas externas o las de bibliotecas estándar, no se pueden editar.

Puedes habilitar un archivo de configuración de estabilidad que tome las clases (con los posibles comodines) que se tratarán como estables.

Para hacerlo, sigue estos pasos:

  1. Navega al archivo build.gradle.kts de la app.
  2. Agrega la opción stabilityConfigurationFile al bloque composeCompiler:
composeCompiler {
    ...

    stabilityConfigurationFile = project.rootDir.resolve("stability_config.conf")
}
  1. Sincroniza el proyecto con los archivos de Gradle.
  2. Abre el archivo stability_config.conf en la carpeta raíz de este proyecto, junto al archivo README.md.
  3. Agrega lo siguiente:
// TODO Codelab task: Make a java.time.LocalDate class stable.
java.time.LocalDate
  1. Vuelve a compilar la app. Si la fecha es la misma, la clase LocalDateTime no hará que el elemento Latest change was YYYY-MM-DD componible se vuelva a componer.

332ab0b2c91617f2.gif

En tu app, puedes extender el archivo que contiene los patrones, de modo que no tendrás que escribir todas las clases que se deben considerar como estables. Así que, en tu caso, puedes usar el comodín java.time.*, que considerará estables a todas las clases incluidas en el paquete, como Instant, LocalDateTime, ZoneId y otras clases de java.time.

Con estos los pasos, no se recompone nada en esta pantalla, excepto el elemento que se agregó o con el que se interaccionó, que es el comportamiento esperado.

12. Felicitaciones

¡Felicitaciones! Optimizaste el rendimiento de una app de Compose. Si bien solo mostramos una pequeña parte de los problemas de rendimiento con los que te puedes encontrar en tu app, aprendiste a detectar otros problemas potenciales y a corregirlos.

¿Qué sigue?

Si no generaste un perfil de Baseline para tu app, te recomendamos que lo hagas.

Puedes seguir el codelab Cómo mejorar el rendimiento de la app con perfiles de Baseline. Si quieres obtener más información sobre la configuración de comparativas, consulta este Cómo inspeccionar el rendimiento de la app con Macrobenchmark.

Más información