En esta página, aprenderás sobre el ciclo de vida de un elemento componible y la forma en la que Compose decide si uno de estos elementos necesita o no recomposición.
Descripción general del ciclo de vida
Como se mencionó en la documentación de administración de estados, un elemento Composition describe la IU de tu app y se produce ejecutando elementos que admiten composición. Es una estructura de árbol de esos elementos que describe tu IU.
Cuando Jetpack Compose ejecute tus elementos componibles por primera vez, durante la composición inicial, mantendrá un registro de los elementos componibles a los que llamas para describir tu IU en un objeto Composition. Luego, cuando cambie el estado de la app, Jetpack Compose programará una recomposición. Este evento se genera cuando Jetpack Compose vuelve a ejecutar los elementos componibles que pueden haberse modificado en respuesta a cambios de estado y, luego, actualiza la composición para reflejar los cambios.
Un objeto Composition solo puede producirse mediante una composición inicial y actualizarse mediante la recomposición. La única forma de modificar un objeto Composition es mediante la recomposición.
Figura 1: Ciclo de vida de un elemento componible en el objeto Composition. Ingresa al objeto Composition, se vuelve a componer 0 o más veces, y deja el objeto.
Por lo general, la recomposición se activa mediante un cambio en un objeto State<T>
. Compose realiza un seguimiento de estas modificaciones y ejecuta todos los elementos componibles en el objeto Composition que lee ese State<T>
determinado, y cualquier elemento componible que no se pueda omitir.
Si se llama varias veces a un elemento componible, se colocan varias instancias en el objeto Composition. Cada llamada tiene su propio ciclo de vida en Composition.
@Composable fun MyComposable() { Column { Text("Hello") Text("World") } }
Figura 2: Representación de MyComposable
en Composition Si se llama varias veces a un elemento componible, se colocan varias instancias en el objeto Composition. Si un elemento tiene un color diferente, significa que pertenece a otra instancia.
Anatomía de un elemento componible en Composition
La instancia de un elemento componible en Composition se identifica mediante su sitio de llamadas. El compilador de Compose considera que cada sitio de llamadas es diferente. Invocar a elementos componibles desde varios sitios de llamadas creará varias instancias del elemento componible en Composition.
Si, durante una recomposición, un elemento componible llama a un elemento diferente del que invocó durante la composición anterior, Compose identificará qué elementos componibles fueron llamados o no. En el caso de los elementos llamados en ambas composiciones, Compose evitará volver a componerlos si sus entradas no cambiaron.
Preservar la identidad es fundamental para asociar efectos secundarios con su elemento componible a fin de que puedan completarse correctamente en lugar de reiniciarse para cada recomposición.
Consulta el siguiente ejemplo:
@Composable fun LoginScreen(showError: Boolean) { if (showError) { LoginError() } LoginInput() // This call site affects where LoginInput is placed in Composition } @Composable fun LoginInput() { /* ... */ } @Composable fun LoginError() { /* ... */ }
En el fragmento de código anterior, LoginScreen
llamará condicionalmente al elemento componible LoginError
y siempre llamará al elemento componible LoginInput
. Cada llamada tiene un sitio de llamada y una posición en el código fuente únicos, que el compilador utilizará para identificarla de forma única.
Figura 3: Representación de LoginScreen
en Composition cuando cambia el estado y se genera una recomposición. El mismo color significa que no se volvió a componer.
Aunque LoginInput
pasó de ser llamado en primer lugar al segundo, se conservará la instancia de LoginInput
entre las recomposiciones. Además, debido a que LoginInput
no tiene ningún parámetro que haya cambiado en la recomposición, Compose omitirá la llamada a LoginInput
.
Agrega información adicional para ayudar a las recomposiciones inteligentes
Si se llama varias veces en simultáneo a un elemento componible, se agregará también muchas veces a Compose. Cuando se llama a un elemento componible muchas veces desde el mismo sitio de llamadas, Compose no tiene información para identificar de forma exclusiva cada llamada a ese elemento, por lo que se usa el orden de ejecución, además del sitio de llamada, para que las instancias sean distintas. En ocasiones, este comportamiento es todo lo que se necesita, pero, en algunos casos, puede causar comportamientos no deseados.
@Composable fun MoviesScreen(movies: List<Movie>) { Column { for (movie in movies) { // MovieOverview composables are placed in Composition given its // index position in the for loop MovieOverview(movie) } } }
En el ejemplo anterior, Compose utiliza el orden de ejecución, además del sitio de llamada, para que las instancias sean distintas en Composition. Si se agrega una nueva movie
a la parte inferior de la lista, Compose puede volver a usar las instancias que ya se encuentran en Composition, dado que su ubicación en la lista no cambió y, por lo tanto, la entrada movie
es la misma para esas instancias.
Figura 4: Representación de MoviesScreen
en Composition cuando se agrega un nuevo elemento a la parte inferior de la lista. Los elementos componibles MovieOverview
de Composition pueden reutilizarse. Si se muestra el mismo color en MovieOverview
, significa que no se volvió a componer el elemento componible.
Sin embargo, si la lista movies
cambia cuando se agrega a un elemento a la parte superior o a la mitad de la lista, o bien si se lo quita o se reorganiza el orden, se generará una recomposición en todas las llamadas a MovieOverview
cuyo parámetro de entrada haya cambiado de posición en la lista. Es muy importante si, por ejemplo, MovieOverview
recupera una imagen de película con un efecto secundario. Si la recomposición ocurre mientras el efecto está en curso, se cancelará y comenzará de nuevo.
@Composable fun MovieOverview(movie: Movie) { Column { // Side effect explained later in the docs. If MovieOverview // recomposes, while fetching the image is in progress, // it is cancelled and restarted. val image = loadNetworkImage(movie.url) MovieHeader(image) /* ... */ } }
Figura 5: Representación de MoviesScreen
en Composition cuando se agrega un nuevo elemento a la lista. Los elementos componibles MovieOverview
no se pueden volver a usar, y se reiniciarán todos los efectos secundarios. Si se muestra un color diferente en MovieOverview
, significa que se volvió a componer el elemento componible.
Se recomienda pensar en la identidad de la instancia MovieOverview
como vinculada a la identidad de la movie
que se pasa a ella. Si se reordena la lista de películas, lo ideal sería reordenar las instancias en el árbol de Composition, en lugar de volver a componer cada MovieOverview
componible con una instancia de película diferente. Compose proporciona una forma de indicarle al entorno de ejecución qué valores deseas usar para identificar una parte del árbol determinada: key
.
Si unes un bloque de código con una llamada al elemento componible clave con uno o más valores pasados, se combinarán esos valores para que se los use en la composición de esa instancia. El valor de una key
no necesita ser globalmente único, sino que solo debe ser único entre las invocaciones de elementos componibles en el sitio de llamada. Por lo tanto, en este ejemplo, cada movie
debe tener una key
que sea única entre los objetos movies
; no pasa nada si comparte key
con algún otro elemento componible de la app.
@Composable fun MoviesScreenWithKey(movies: List<Movie>) { Column { for (movie in movies) { key(movie.id) { // Unique ID for this movie MovieOverview(movie) } } } }
Con el ejemplo anterior, incluso si los elementos de la lista cambian, Compose reconoce llamadas individuales a MovieOverview
y puede volver a usarlas.
Figura 6: Representación de MoviesScreen
en Composition cuando se agrega un nuevo elemento a la lista. Dado que los elementos componibles MovieOverview
tienen claves únicas, Compose reconoce qué instancias de MovieOverview
no cambiaron y puede volver a usarlas. Sus efectos secundarios seguirán ejecutándose.
Algunos elementos componibles tienen compatibilidad integrada con el elemento key
. Por ejemplo, LazyColumn
acepta especificar una key
personalizada en el DSL de items
.
@Composable fun MoviesScreenLazy(movies: List<Movie>) { LazyColumn { items(movies, key = { movie -> movie.id }) { movie -> MovieOverview(movie) } } }
Cómo omitir procesos si las entradas no cambiaron
Durante la recomposición, algunas funciones de componibilidad aptas pueden tener su ejecución se omitirán por completo si sus entradas no han cambiado desde la de los datos.
Una función de componibilidad es apta para omitir, a menos que ocurra lo siguiente:
- La función tiene un tipo de datos que no es
Unit
que se muestra - La función tiene la anotación
@NonRestartableComposable
.@NonSkippableComposable
- Un parámetro obligatorio es de un tipo no estable
Hay un modo experimental del compilador, Strong Skipping, que relaja el último requisito.
Para que un tipo se considere estable, debe cumplir con el siguiente contrato:
- El resultado de
equals
para dos instancias siempre será el mismo para las mismas dos instancias. - Si cambia una propiedad pública del tipo, se notificará a Compose.
- También son estables todos los tipos de propiedades públicas.
Hay algunos tipos comunes importantes que se incluyen en este contrato que el compilador de Compose tratará como estables, incluso si no se los marca explícitamente como estables mediante la anotación @Stable
:
- Todos los tipos de valores primitivos:
Boolean
,Int
,Long
,Float
,Char
y demás - Strings
- Todos los tipos de funciones (lambdas)
Todos estos tipos pueden seguir el contrato de estabilidad porque son inmutables. Debido a que los tipos inmutables no cambian, no deben notificar a Compose del cambio, por lo que es mucho más fácil seguir este contrato.
Un tipo notable que es estable, pero también mutable, es el tipo MutableState
de Compose. Si un valor se retiene en un MutableState
, se considera que el objeto de estado general es estable, ya que Compose recibirá una notificación de cualquier cambio en la propiedad .value
de State
.
Cuando todos los tipos pasados como parámetros de un elemento componible son estables, los valores de los parámetros se comparan para determinar su igualdad según la posición del elemento componible en el árbol de IU. La recompensación se omite si todos los valores no cambian desde la llamada anterior.
Compose considera que un tipo es estable solo si puede probarlo. Por ejemplo, una interfaz se trata generalmente como no estable, y los tipos con propiedades públicas mutables cuya implementación podría ser inmutable tampoco son estables.
Si Compose no puede inferir que un tipo es estable, pero quieres forzar a Compose para que lo considere estable, márcalo con la anotación @Stable
.
// Marking the type as stable to favor skipping and smart recompositions. @Stable interface UiState<T : Result<T>> { val value: T? val exception: Throwable? val hasError: Boolean get() = exception != null }
En el fragmento de código anterior, dado que UiState
es una interfaz, Compose puede considerar que este tipo no es estable. Si agregas la anotación @Stable
, le indicas a Compose que este tipo es estable, lo que le permite priorizar las recomposiciones inteligentes. Eso también significa que Compose tratará todas sus implementaciones como estables si la interfaz se usa como el tipo de parámetro.
Recomendaciones para ti
- Nota: El texto del vínculo se muestra cuando JavaScript está desactivado
- El estado y Jetpack Compose
- Efectos secundarios en Compose
- Cómo guardar el estado de la IU en Compose