El objetivo de los efectos secundarios es permitir la ejecución de operaciones no reacionadas con la IU que cambian el estado de la aplicación fuera de una función Composable de una manera controlada y previsible.
Introducción
Los efectos secundarios, como actualizar una base de datos u obtener datos remotos, deben mantenerse separados de la lógica de representación de la interfaz de usuario para mejorar el rendimiento y el mantenimiento del código.
Compose ofrece varias funciones componibles, tales como SideEffect
, LaunchedEffect
y DisposableEffect
, que te permiten gestionar los efectos secundarios de manera eficaz, desacoplándolos de la lógica de representación de la interfaz de usuario y ejecutándolos en un ámbito de corrutina independiente.
Crea un proyecto compose-side-effects
.
Tienes un proyecto de ayuda: https://gitlab.com/xtec/kotlin/compose/side-effects/
SideEffect
SideEffect
es una función Composable que nos permite ejecutar un efecto secundario cuando su Composable padre se recompone.
- Registro y análisis
- Realizar una inicialización única, como configurar una conexión a un dispositivo Bluetooth, cargar datos de un archivo o inicializar una biblioteca.
Un efecto secundario es una operación que no afecta directamente a la interfaz de usuario, como el registro, el análisis o la actualización del estado externo. Esta función es útil para ejecutar operaciones que no dependen del estado o propiedades del Composable.
Cuando se recompone un Composable, se vuelve a ejecutar todo el código dentro de la función Composable, incluidos los efectos secundarios. Sin embargo, la interfaz de usuario sólo se actualizará con los cambios que se hayan realizado en el estado o propiedades del Composable.
Para utilizar SideEffect
, debemos llamarlo dentro de una función Composable y pasar una lambda que contenga el efecto secundario que queremos ejecutar.
Modifica el archivo App.kt
:
@Composable
fun App() {
MaterialTheme {
Counter()
}
}
@Composable
fun Counter() {
// Definim una variable d'estat
val count = remember { mutableStateOf(0)}
// Utilitzem un efecte secundari per registrar el valor actual de count
SideEffect {
println("Count is ${count.value}")
}
Column {
Button(onClick = {count.value++}) {
Text("Increase count")
}
// Cada cop que es modifica l'estat, es modifica el text i es torna a executar una recomposició
Text("Counter ${count.value}")
}
}
En este ejemplo, la función SideEffect
registra el valor actual de la variable de estado count
siempre que la función Counter
se recompone. Esto es útil para depurar y supervisar el comportamiento del Composable.
Ten en cuenta que el efecto secundario sólo se activa cuando se recompone la función componible actual y no para ninguna función componible imbricada. Esto significa que si tienes una función Composable que llama a otra función Composable, el SideEffect
de la función Composable externa no se activará cuando se recompone la función Composable interna.
Para entender esto, modifica el código:
@Composable
fun Counter() {
// Definim una variable d'estat
val count = remember { mutableStateOf(0) }
// Utilitzem un efecte secundari per registrar el valor actual de count
SideEffect {
println("Count is ${count.value}")
}
Column {
Button(onClick = { count.value++ }) {
// Aquesta recomposició no activa l'efecte secundari extern quan l'usuari apreta el butó
Text("Increase Count ${count.value}")
}
// Text("Counter ${count.value}")
}
}
En este código, cuando se inicia la aplicación por primera vez, se compone la función Composable Counter
y el SideEffect
registra el valor inicial de count
en la consola.
Cuando haces clic en el Button
, el Text
se recompone con el nuevo valor de count
, pero esto no vuelve a activar el SideEffect
porqué el Counter
no se recompone.
Ahora, añadimos un efecto secundario interno para ver cómo funciona:
@Composable
fun Counter() {
// Definim una variable d'estat
val count = remember { mutableStateOf(0) }
// Utilitzem un efecte secundari per registrar el valor actual de count
SideEffect {
println("Outer Count is ${count.value}")
}
Column {
Button(onClick = { count.value++ }) {
SideEffect {
println("Inner Count is ${count.value}")
}
// Aquesta recomposició no activa l'efecte secundari extern quan l'usuari apreta el butó
Text("Increase Count ${count.value}")
}
}
}
Con este código, si se hace clic en el botón, la salida será esta:
Outer Count is 0
Inner Count is 0
Inner Count is 1
Inner Count is 2
Inner Count is 3
Seguro que ahora sabes el motivo 😀
LaunchedEffect
LaunchedEffect
es una función componible que ejecuta un efecto secundario en un ámbito de corrutina independiente. Esta función es útil para ejecutar operaciones que pueden tardar mucho tiempo, como obtener datos a través de la red o animaciones, sin bloquear el hilo de la IU.
- Obtención de datos de una red
- Realización de procesamiento de imágenes
- Actualización de una base de datos
A continuación tienes un ejemplo:
@Composable
fun FetchComposable() {
var isLoading by remember { mutableStateOf(false) }
var data by remember { mutableStateOf(listOf<String>()) }
// Definim un LaunchEffect per realitzar una operació asícnrona que tardarà molt de temps en completar-se
// LaunchEffect es cancelarà i és tornarà a executar si `isLoading` canvia.
LaunchedEffect(isLoading) {
if (isLoading) {
// Realitza una tasca que tarda molt de temps en executar-se, com obtenir dades desde una xarxa
val newData = fetchData()
// Actualitzem l'estat amb les noves dades
data = newData
isLoading = false
}
}
Column {
Button(onClick = { isLoading =true}) {
Text("Fetch Data")
}
if (isLoading) {
// Mostra un indicador de progress
CircularProgressIndicator()
} else {
// Mostra les dades
LazyColumn {
items(data) { item ->
Text(item)
}
}
}
}
}
En este ejemplo, la función LaunchedEffect
ejecuta una llamada de red para obtener datos de una API cuando la variable de estado isLoading
tiene el valor true. La función se ejecuta en un ámbito de corrutina independiente, permitiendo que la interfaz de usuario siga respondiendo mientras se realiza la operación.
La función LaunchedEffect
toma dos parámetros: key
, que se establece en isLoading
, y block
que es una lambda que define el efecto secundario a ejecutar.
En este caso, el bloque lambda invoca la función fetchData()
, que simula una llamada de red que suspende la corrutina durante 2 segundos. Una vez que se obtienen los datos, actualiza la variable de estado data
y se pone isLoading
a false, escondiendo el indicador de carga y mostrando los datos obtenidos.
¿Cuál es la lógica detrás del parámetro key?
El parámetro key
en LaunchedEffect
se utiliza para identificar la instancia LaunchedEffect
y evitar que se recomponga innecesariamente.
Cuando se recompone un Composable, Compose determina si debe volver a dibujarse. Si el estado o las propiedades de un Composable han cambiado, o si un Composable ha invocado invalidate
, Compose vuelve a dibujar el Composable. Redibujar un Composable puede ser una operación costosa, sobre todo si el Composable contiene operaciones de larga duración o efectos secundarios que no es necesario que se vuelvan a ejecutar cada vez que se recompone el Composable.
Al proporcionar un parámetro key
a LaunchedEffect
, puedes especificar un valor que identifique de forma única la instancia LaunchedEffect
. Si el valor del parámetro key
cambia, Compose considerará la instancia LaunchedEffect
como una nueva instancia y volverá a ejecutar el efecto secundario. Si el valor del parámetro key sigue siendo el mismo, Compose saltará la ejecución del efecto secundario y reutilizará el resultado anterior, evitando recomposiciones innecesarias.
También puede utilizar varias "keys" para LaunchedEffect
:
// Utilitzeu un UUID aleatori com a clau per a LaunchedEffect
val key = remember { UUID.randomUUID().toString() }
LaunchedEffect(key, isLoading) {
....
}
DisposableEffect
DisposableEffect
es una función Composable que ejecuta un efecto secundario cuando se representa por primera vez el Composable principal y elimina el efecto cuando el Composable se elimina de la jerarquía de IU.
Esta función es útil para gestionar los recursos que deben limpiarse cuando un Composable ya no se utiliza, tales como:
- Añadir y eliminar oyentes de eventos ("event listeners")
- Iniciar y detener animaciones
- Enlazar y desvincular recursos de sensores tales como Camera, LocationManager, etc
- Gestión de conexiones de bases de datos
A continuación tienes un ejemplo:
@Composable
fun TimerScreen() {
var elapsedTime by remember { mutableStateOf(0) }
DisposableEffect(Unit) {
val scope = CoroutineScope(Dispatchers.Default)
val job = scope.launch {
while(true) {
delay(1_000)
elapsedTime +=1
println("Timer is still working $elapsedTime")
}
}
onDispose {
job.cancel()
}
}
Text(
text = "Elapsed Time: $elapsedTime",
modifier = Modifier.padding(16.dp),
fontSize = 24.sp
)
}
En este código, utilizamos DisposableEffect
para lanzar una corrutina que aumenta el valor del estado elapsedTime
cada segundo. También utilizamos DisposableEffect
para garantizar que la corrutina se cancele y los recursos utilizados por la corrutina se limpian cuando el Composable ya no se utiliza.
En la función de limpieza del DisposableEffect
, cancelamos la corrutina utilizando el método cancel()
de la instancia Job
almacenada en job.
La función onDisposese
se llama cuando el Composable se elimina de la jerarquía de la interfaz de usuario y proporciona una forma de limpiar cualquier recurso utilizado por Composable. En este caso, utilizamos onDispose
para cancelar la corrutina y asegurarnos de que se limpia todos los recursos utilizados por la corrutina.
Para comprobar cómo funciona DisposableEffect
, ejecuta este código para ver el resultado:
@Composable
fun RunTimerScreen() {
var isVisible by remember { mutableStateOf(true) }
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Bottom
) {
Spacer(modifier = Modifier.height(10.dp))
Button(onClick = { isVisible = false }) {
Text("Hide the timer")
}
if (isVisible)
TimerScreen()
}
}
Has añadido un nuevo Composable RunTimerScreen
que permite al usuario cambiar la visibilidad de TimerScreen
. Cuando el usuario hace clic en el botón "Hide the timer", el Composable TimerScreense
elimina de la jerarquía de la interfaz de usuario y la corrutina se cancela y limpia.
Si eliminas la invocación a job.cancel()
de la función onDispose
la corrutina continuará ejecutándose incluso cuando el Composable TimerScreen
ya no esté en uso, lo que puede provocar escapes y otros problemas de rendimiento.
Mediante el uso conjunto de DisposableEffect
y CoroutineScope
te aseguras que la corrutina lanzada por CoroutineScope
se cancela y se limpian los recursos cuando el TimerScreen
ya no está en uso.