Gestión Avanzada de Estado en Compose: Effects y Flows
Este artículo explora patrones avanzados de gestión de estado en Jetpack Compose, centrándose en Effects e integración de Flows. Para conceptos fundamentales como mutableStateOf
y state hoisting, consulta nuestro artículo complementario Gestión Básica de Estado en Jetpack Compose.
Entendiendo los Effects en Compose
Los Effects en Compose son herramientas para manejar efectos secundarios y eventos del ciclo de vida de manera compatible con los composables. Exploremos los diferentes tipos de effects y sus casos de uso:
LaunchedEffect: Efectos Secundarios Basados en Corrutinas
LaunchedEffect
lanza una corrutina que se cancela automáticamente cuando el composable sale de la composición:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| @Composable
fun AutoRefreshingList(viewModel: ListViewModel) {
// Inicia una corrutina que actualiza datos cada 30 segundos
LaunchedEffect(Unit) {
while(true) {
viewModel.refreshData()
delay(30_000)
}
}
// El parámetro key controla cuándo debe reiniciarse el efecto
LaunchedEffect(viewModel.searchQuery) {
viewModel.performSearch()
}
}
|
SideEffect: Sincronización con Código No-Compose
SideEffect
se llama en cada recomposición exitosa para sincronizar el estado de Compose con código no-Compose:
1
2
3
4
5
6
7
| @Composable
fun AnalyticsScreen(screenName: String) {
SideEffect {
// Se llama después de cada recomposición exitosa
AnalyticsTracker.trackScreen(screenName)
}
}
|
DisposableEffect: Operaciones de Limpieza
DisposableEffect
maneja la limpieza cuando un composable sale de la composición:
1
2
3
4
5
6
7
8
9
10
11
| @Composable
fun NetworkMonitor(onConnectionChange: (Boolean) -> Unit) {
DisposableEffect(Unit) {
val listener = NetworkListener(onConnectionChange)
listener.register()
onDispose {
listener.unregister()
}
}
}
|
derivedStateOf: Estado Computado
derivedStateOf
crea un estado que se actualiza automáticamente cuando sus dependencias cambian. Es particularmente útil para cálculos basados en umbrales y derivaciones de estado de UI que no deberían disparar recomposiciones en cada pequeño cambio:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
| import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.runtime.derivedStateOf
// Ejemplo que muestra cómo derivedStateOf ayuda a prevenir
// recomposiciones innecesarias al trabajar con estado de scroll
@Composable
fun ScrollableNewsScreen() {
val listState = rememberLazyListState()
// ❌ Sin derivedStateOf
// Estas propiedades se recalcularían en cada cambio de píxel del scroll,
// causando recomposiciones innecesarias de cualquier composable que las lea
// val showScrollToTop = listState.firstVisibleItemIndex > 0
// val isScrollInProgress = listState.isScrollInProgress
// ✅ Con derivedStateOf
// Estos cálculos solo se disparan al cruzar umbrales específicos,
// reduciendo significativamente las recomposiciones innecesarias
val showScrollToTop by remember {
derivedStateOf {
// Solo muestra el botón cuando se ha desplazado más allá del primer elemento
listState.firstVisibleItemIndex > 0
}
}
val shouldLoadMore by remember {
derivedStateOf {
val lastVisibleItem = listState.layoutInfo.visibleItemsInfo.lastOrNull()
// Dispara la paginación cuando el usuario está cerca del final
lastVisibleItem?.index != null &&
lastVisibleItem.index >= listState.layoutInfo.totalItemsCount - 3
}
}
val isScrollingUp by remember {
derivedStateOf {
// Rastrea la dirección del scroll basado en el primer elemento visible
listState.firstVisibleItemScrollOffset < 100
}
}
// Usa estos estados derivados para controlar elementos de UI como:
// - Visibilidad del botón de scroll hacia arriba (showScrollToTop)
// - Carga de paginación (shouldLoadMore)
// - Barra superior colapsable/expandible (isScrollingUp)
NewsScreenContent(
showScrollButton = showScrollToTop,
isScrollingUp = isScrollingUp,
shouldLoadMore = shouldLoadMore
)
}
// Nota: Implementación de NewsScreenContent y otros componentes UI omitidos por brevedad
|
produceState: Convirtiendo Estado No-Compose a State
produceState
convierte fuentes de estado no-Compose en estado de Compose:
1
2
3
4
5
6
7
8
9
10
| @Composable
fun UserProfile(userId: String) {
val user by produceState<User?>(initialValue = null, userId) {
value = userRepository.fetchUser(userId)
awaitDispose {
// Limpieza si es necesaria
}
}
}
|
Integración de Flows con Compose
Cuando trabajamos con Flows en Compose, necesitamos convertirlos en estado de Compose. Jetpack Compose proporciona dos funciones principales para este propósito: collectAsState
y collectAsStateWithLifecycle
.
collectAsState: Recolección Básica de Flow
collectAsState
convierte un Flow en un State de Compose:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| @Composable
fun UserProfile(viewModel: UserViewModel) {
// Recolección básica
val name by viewModel.nameFlow.collectAsState()
// Con valor inicial
val count by viewModel.countFlow.collectAsState(initial = 0)
// Manejando flows nulables
val user by viewModel.userFlow.collectAsState(initial = null)
Text("Nombre: $name")
Text("Contador: $count")
user?.let { Text("Usuario: ${it.name}") }
}
|
collectAsStateWithLifecycle: Recolección Consciente del Ciclo de Vida
collectAsStateWithLifecycle
es la forma recomendada de recolectar flows en Compose ya que respeta el ciclo de vida del composable. Proporciona varios beneficios sobre collectAsState
:
- Detiene automáticamente la recolección cuando el composable no está visible
- Reanuda la recolección cuando el composable vuelve a ser visible
- Reduce el procesamiento innecesario y el consumo de batería
- Previene fugas de memoria limpiando adecuadamente los recursos
- Permite un control preciso sobre cuándo debe ocurrir la recolección
Aquí te mostramos cómo usarlo:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
| // Tipos de datos para el ejemplo
data class UserUpdate(
val id: String,
val message: String,
val timestamp: Long
)
@Composable
fun UserUpdates(viewModel: UserViewModel) {
// Recolección básica consciente del ciclo de vida
val updates by viewModel.userUpdatesFlow
.collectAsStateWithLifecycle(initialValue = emptyList())
// Estado de ciclo de vida personalizado
val notifications by viewModel.notificationsFlow
.collectAsStateWithLifecycle(
initialValue = emptyList(),
lifecycle = lifecycle,
minActiveState = Lifecycle.State.RESUMED // Solo recolecta cuando está RESUMED
)
LazyColumn {
items(updates) { update: UserUpdate ->
UpdateItem(update)
}
}
}
|
Conclusión
La gestión avanzada de estado en Compose requiere entender tanto los Effects como la integración de Flows. Los Effects ayudan a manejar efectos secundarios y eventos del ciclo de vida, mientras que la integración adecuada de Flows asegura una recolección eficiente de estado que respeta el ciclo de vida de Android.
Recuerda:
- Elegir el Effect apropiado para tu caso de uso
- Usar collectAsStateWithLifecycle para la integración de Flows al recolectar de flows
- Probar exhaustivamente tu implementación de gestión de estado
Para conceptos fundamentales de gestión de estado, consulta nuestro artículo complementario Gestión Básica de Estado en Jetpack Compose.