Compite en el #AndroidDevChallenge de Jetpack Compose para ganar uno de los más de 1,000 premios, entre ellos, un Google Pixel 5. Obtén más información

Ciclo de vida y efectos secundarios

Los elementos componibles no deben tener efectos secundarios. Sin embargo, cuando sea necesario para mutar el estado de la app, se los debe llamar desde un entorno controlado que reconozca el ciclo de vida del elemento componible. En esta página, aprenderás sobre el ciclo de vida de un elemento componible y las diferentes API de efectos secundarios que brinda Jetpack Compose.

Ciclo de vida de un elemento componible

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.

Diagrama que muestra el ciclo de vida de un elemento componible

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")
    }
}

Diagrama que muestra la disposición jerárquica de los elementos en el fragmento de código anterior

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() { /* ... */ }

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.

Diagrama en el que se muestra cómo se recompone el código anterior si se cambia la marca showError a &quot;true&quot;. Se agrega el elemento componible LoginError, pero los demás elementos no se vuelven a componer.

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.

Diagrama que muestra cómo se recompone el código anterior si se agrega un elemento nuevo al final de la lista. Los otros elementos de la lista no cambiaron de posición y no se vuelven a componer.

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)

        /* ... */
    }
}

Diagrama que muestra cómo se recompone el código anterior si se agrega un elemento nuevo a la parte superior de la lista. Todos los demás elementos de la lista cambian de posición y deben volver a componerse.

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 MoviesScreen(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.

Diagrama que muestra cómo se recompone el código anterior si se agrega un elemento nuevo a la parte superior de la lista. Dado que los elementos de lista se identifican mediante claves, Compose sabe que no debe volver a componerlos, incluso si cambiaron sus posiciones.

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 MoviesScreen(movies: List<Movie>) {
    LazyColumn {
        items(movies, key = { movie -> movie.id }) { movie ->
            MovieOverview(movie)
        }
    }
}

Cómo omitir procesos si las entradas no cambiaron

Si un elemento componible ya se encuentra en Composition, puede omitir la recomposición si todas las entradas son estables y no cambiaron.

Un tipo 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 composición tratará como @Stable, incluso si no se los marca explícitamente como @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 @Stable 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.

Casos de uso de los efectos y estados

Como se explica en la documentación Cómo pensar en Compose, los elementos componibles deben tener efectos secundarios. Cuando necesites realizar cambios en el estado de la app (como se describe en el documento Cómo administrar la documentación de estados), deberás usar las API de Effects para que esos efectos secundarios se ejecuten de manera predecible.

Debido a las diferentes posibilidades que ofrecen los efectos en Compose, pueden ser fácilmente sobrecargados. Asegúrate de que el trabajo que realices en ellos esté relacionado con la IU y no divida el flujo de datos unidireccional, como se explica en la documentación de administración de estados.

LaunchedEffect: Ejecuta funciones de suspensión en el alcance de un elemento componible

Para llamar a funciones de suspensión de forma segura dentro de un elemento componible, usa el objeto LaunchedEffect compatible. Cuando LaunchedEffect ingresa a Composition, inicia una corrutina con el bloque de código pasado como un parámetro. La corrutina se cancelará si LaunchedEffect sale de la composición. Si se recompone LaunchedEffect con diferentes claves (consulta a continuación la sección Cómo reiniciar efectos), la corrutina existente se cancelará y se iniciará la nueva función de suspensión en una corrutina nueva.

Por ejemplo, mostrar una Snackbar en una Scaffold se realiza con la función SnackbarHostState.showSnackbar, que es una suspensión.

@Composable
fun MyScreen(
    state: UiState<List<Movie>>,
    scaffoldState: ScaffoldState = rememberScaffoldState()
) {

    // If the UI state contains an error, show snackbar
    if (state.hasError) {

        // `LaunchedEffect` will cancel and re-launch if `scaffoldState` changes
        LaunchedEffect(scaffoldState.snackbarHostState) {
            // Show snackbar using a coroutine, when the coroutine is cancelled the
            // snackbar will automatically dismiss. This coroutine will cancel whenever
            // `state.hasError` is false, and only start when `state.hasError`
            // is true (due to the above if-check), or if `scaffoldState` changes.
            scaffoldState.snackbarHostState.showSnackbar(
                message = "Error message",
                actionLabel = "Retry message"
            )
        }
    }

    Scaffold(scaffoldState = scaffoldState) {
        /* ... */
    }
}

En el código anterior, una corrutina se activa si el estado contiene un error y se cancela cuando no lo tiene. Como el sitio de llamadas de LaunchedEffect está dentro de una declaración "if", cuando la declaración es "false", si LaunchedEffect estaba en Composition, se la quitará y, por lo tanto, se cancelará la corrutina.

RememberCoroutineScope: Obtén un alcance compatible con la composición para iniciar una corrutina fuera de un elemento componible

Dado que LaunchedEffect es una función que admite composición, solo se puede usar dentro de otras funciones de ese tipo. Para iniciar una corrutina fuera de un elemento componible, pero con un alcance definido que se cancelará automáticamente una vez que salga de la composición, usa rememberCoroutineScope. Además, puedes usar rememberCoroutineScope cada vez que necesites controlar el ciclo de vida de una o más corrutinas de forma manual; por ejemplo, para cancelar una animación cuando se produce un evento de usuario.

rememberCoroutineScope es una función que admite composición que muestra un CoroutineScope vinculado al punto de Composition al que se llama. El alcance se cancelará cuando la llamada salga de Composition.

Siguiendo el ejemplo anterior, puedes usar este código para mostrar una Snackbar cuando el usuario presiona un Button:

@Composable
fun MoviesScreen(scaffoldState: ScaffoldState = rememberScaffoldState()) {

    // Creates a CoroutineScope bound to the MoviesScreen's lifecycle
    val scope = rememberCoroutineScope()

    Scaffold(scaffoldState = scaffoldState) {
        Column {
            /* ... */
            Button(
                onClick = {
                    // Create a new coroutine in the event handler
                    // to show a snackbar
                    scope.launch {
                        scaffoldState.snackbarHostState
                            .showSnackbar("Something happened!")
                    }
                }
            ) {
                Text("Press me")
            }
        }
    }
}

RemindUpdatedState: Haz referencia a un valor en un efecto que no se debe reiniciar si cambia el valor

LaunchedEffect se reinicia cuando cambia uno de los parámetros clave. Sin embargo, en algunas situaciones, puede que quieras capturar un valor en tu efecto que, si cambia, no quieres que se reinicie. Para ello, debes usar rememberUpdatedState a fin de crear una referencia a ese valor que se puede capturar y actualizar. Este enfoque es útil para efectos que contienen operaciones de larga duración que pueden ser costosas o imposibles de recrear y reiniciar.

Por ejemplo, supongamos que tu app tiene una LandingScreen que desaparece después de un tiempo. Incluso si se vuelve a componer LandingScreen, el efecto que espera unos segundos y notifica que ya transcurrió el tiempo no debería reiniciarse:

@Composable
fun LandingScreen(onTimeout: () -> Unit) {

    // This will always refer to the latest onTimeout function that
    // LandingScreen was recomposed with
    val currentOnTimeout by rememberUpdatedState(onTimeout)

    // Create an effect that matches the lifecycle of LandingScreen.
    // If LandingScreen recomposes, the delay shouldn't start again.
    LaunchedEffect(true) {
        delay(SplashWaitTimeMillis)
        currentOnTimeout()
    }

    /* Landing screen content */
}

Para crear un efecto que coincida con el ciclo de vida del sitio de llamada, se pasa como parámetro una constante inmutable, como Unit o true. En el código anterior, se usa LaunchedEffect(true). Para asegurarte de que la lambda onTimeout siempre contenga el valor más reciente con el que se volvió a componer LandingScreen, onTimeout debe unirse a la función rememberUpdatedState. El objeto State que se muestra, currentOnTimeout en el código, se debe usar en el efecto.

DisposableEffect: Efectos que requieren limpieza

Para los efectos secundarios que se deben limpiar después de que cambian las claves o si el elemento componible deja Composition, usa DisposableEffect. Si cambian las claves DisposableEffect, el elemento componible debe descartar (eliminar) su efecto actual y restablecerlo llamando otra vez al efecto.

Por ejemplo, un elemento OnBackPressedCallback debe estar registrado para escuchar cuando se presione el botón Atrás en un OnBackPressedDispatcher. Para escuchar esos eventos en Compose, utiliza un DisposableEffect a fin de registrar y dejar de registrar la devolución de llamada cuando sea necesario.

@Composable
fun BackHandler(backDispatcher: OnBackPressedDispatcher, onBack: () -> Unit) {

    // Safely update the current `onBack` lambda when a new one is provided
    val currentOnBack by rememberUpdatedState(onBack)

    // Remember in Composition a back callback that calls the `onBack` lambda
    val backCallback = remember {
        // Always intercept back events. See the SideEffect for
        // a more complete version
        object : OnBackPressedCallback(true) {
            override fun handleOnBackPressed() {
                currentOnBack()
            }
        }
    }

    // If `backDispatcher` changes, dispose and reset the effect
    DisposableEffect(backDispatcher) {
        // Add callback to the backDispatcher
        backDispatcher.addCallback(backCallback)

        // When the effect leaves the Composition, remove the callback
        onDispose {
            backCallback.remove()
        }
    }
}

En el código anterior, el efecto agregará la backCallback recordada a backDispatcher. Si cambia backDispatcher, el efecto se descarta y se reinicia nuevamente.

Un DisposableEffect debe incluir una cláusula onDispose como declaración final en su bloque de código. De lo contrario, el IDE mostrará un error de tiempo de compilación.

SideEffect: Publica el estado de Compose en código no componible

Para compartir el estado de Compose con objetos que no administra la composición, usa el elemento componible SideEffect, ya que se invoca en cada recomposición exitosa.

Tomando el código BackHandler anterior como ejemplo, a fin de comunicar si se debe habilitar o no la devolución de llamada, usa SideEffect para actualizar su valor.

@Composable
fun BackHandler(
    backDispatcher: OnBackPressedDispatcher,
    enabled: Boolean = true, // Whether back events should be intercepted or not
    onBack: () -> Unit
) {
    /* ... */
    val backCallback = remember { /* ... */ }

    // On every successful composition, update the callback with the `enabled` value
    // to tell `backCallback` whether back events should be intercepted or not
    SideEffect {
        backCallback.isEnabled = enabled
    }

    /* Rest of the code */
}

produceState: Convierte un estado que no sea de Compose en uno que sí lo sea

produceState lanza una corrutina orientada al objeto Composition que puede enviar valores a un State que se muestra. Úsalo para convertir el estado que no sea de Compose en uno que sí lo sea, por ejemplo, llevando el estado externo controlado por suscripciones, como Flow, LiveData o RxJava, a Composition.

El productor se lanza cuando produceState ingresa a Composition y se cancela cuando sale de allí. El State que se muestra se combina; si estableces el mismo valor, no se activará una recomposición.

Por más que produceState cree una corrutina, también se puede usar para observar fuentes de datos que no están suspendidas. Para quitar la suscripción a esa fuente, usa la función awaitDispose.

En el siguiente ejemplo, se muestra cómo usar produceState para cargar una imagen desde la red. La función que admite composición loadNetworkImage muestra un State que se puede usar en otros elementos componibles.

@Composable
fun loadNetworkImage(
    url: String,
    imageRepository: ImageRepository
): State<Result<Image>> {

    // Creates a State<T> with Result.Loading as initial value
    // If either `url` or `imageRepository` changes, the running producer
    // will cancel and will be re-launched with the new keys.
    return produceState(initialValue = Result.Loading, url, imageRepository) {

        // In a coroutine, can make suspend calls
        val image = imageRepository.load(url)

        // Update State with either an Error or Success result.
        // This will trigger a recomposition where this State is read
        value = if (image == null) {
            Result.Error
        } else {
            Result.Success(image)
        }
    }
}
.

derivedStateOf: Convierte uno o varios objetos de estado en otro estado

Usa derivedStateOf cuando se calcula o deriva un estado determinado de otros objetos de estado. El uso de esta función garantiza que el cálculo solo ocurrirá cuando cambie uno de los estados del cálculo.

En el siguiente ejemplo, se muestra una lista básica de pendientes cuyas tareas con palabras clave de prioridad alta definidas por usuarios aparecen primero:

@Composable
fun TodoList(
    highPriorityKeywords: List<String> = listOf("Review", "Unblock", "Compose")
) {
    val todoTasks = remember { mutableStateListOf<String>() }

    // Calculate high priority tasks only when the todoTasks or
    // highPriorityKeywords change, not on every recomposition
    val highPriorityTasks by remember(todoTasks, highPriorityKeywords) {
        derivedStateOf {
            todoTasks.filter { it.containsWord(highPriorityKeywords) }
        }
    }

    Box(Modifier.fillMaxSize()) {
        LazyColumn {
            items(highPriorityTasks) { /* ... */ }
            items(todoTasks) { /* ... */ }
        }
        /* Rest of the UI where users can add elements to the list */
    }
}

En el código anterior, derivedStateOf garantiza que cuando cambien todoTasks o highPriorityKeywords, se realizará el cálculo highPriorityTasks y se actualizará la IU según corresponda. Dado que el filtrado para calcular highPriorityTasks puede ser costoso, solo se debe ejecutar cuando cambia alguna de las listas, no en todas las recomposiciones.

Además, una actualización del estado producida por derivedStateOf no hace que se vuelva a componer el elemento componible en el que se declara. Compose solo vuelve a componer esos elementos componibles donde el estado que se muestra es leído (en el ejemplo, dentro de LazyColumn).

Cómo reiniciar efectos

Algunos efectos de Compose, como LaunchedEffect, produceState o DisposableEffect, toman una cantidad variable de argumentos, claves, que se usan para cancelar el efecto de ejecución y comenzar uno nuevo con las nuevas claves.

El formato típico de estas API es el siguiente:

EffectName(restartIfThisKeyChanges, orThisKey, orThisKey, ...) { block }

Debido a las sutilezas de este comportamiento, pueden generarse problemas si los parámetros usados para reiniciar el efecto no son los adecuados:

  • Reiniciar los efectos con menor frecuencia de la debida podría generar errores en tu app.
  • Reiniciarlos en exceso podría hacer que su uso sea ineficiente.

Como regla general, las variables inmutables y mutables que se usan en el bloque de código de efecto deben agregarse como parámetros para el efecto componible. Además de esos, se pueden agregar más parámetros para forzar el reinicio del efecto. Si el cambio de una variable no debería causar el efecto del reinicio, la variable debe unirse en rememberUpdatedState. Si la variable no cambia porque se une a un remember sin claves, no necesitas pasar la variable como una clave para el efecto.

En el código DisposableEffect que se muestra arriba, el efecto toma como parámetro el backDispatcher que se usa en su bloque, ya que cualquier cambio él provocaría que se reinicie el efecto.

@Composable
fun BackHandler(backDispatcher: OnBackPressedDispatcher, onBack: () -> Unit) {
    /* ... */
    val backCallback = remember { /* ... */ }

    DisposableEffect(backDispatcher) {
        backDispatcher.addCallback(backCallback)
        onDispose {
            backCallback.remove()
        }
    }
}

backCallback no es necesaria como una clave DisposableEffect porque su valor nunca cambiará en Composition. Queda unido a un remember sin claves. Si no se pasa backDispatcher como parámetro y cambia, se volverá a componer BackHandler, pero no se descartará DisposableEffect ni se reiniciará. Eso generará problemas, ya que se usará un backDispatcher incorrecto a partir de ese momento.

Constantes como claves

Puedes usar una constante como true como clave de efecto para que siga el ciclo de vida del sitio de llamadas. Hay casos de uso válidos para eso, como el ejemplo de LaunchedEffect que se muestra arriba. Sin embargo, antes de hacer eso, piensa dos veces y asegúrate de que sea lo que necesitas.