Lista y sincroniza preórdenes con StateFlow

Resumen

Mostrar una lista de preórdenes en una app Android con Jetpack Compose implica más que pintar datos en pantalla. Necesitas un ViewModel que exponga el estado de la UI, una composable que reaccione a ese estado y acciones para sincronizar o eliminar registros locales. Aquí verás cómo conectar esas piezas paso a paso, ideal si ya trabajas con Room, StateFlow y arquitectura limpia en Android.

¿Cómo se construye el estado de la UI para una lista de preórdenes?

El primer paso es definir una data class que represente todo lo que la vista necesita saber en cualquier momento. En lugar de exponer la lista cruda, encapsulas tres campos clave dentro de un PreOrderState: isLoading para el estado de carga, data con la lista de preórdenes y isError para fallos.

kotlin data class PreOrderState( val isLoading: Boolean = false, val data: List<PreOrder> = emptyList(), val isError: Boolean = false )

Con esa estructura puedes reusar el PreOrderViewModel de la clase anterior [01:05]. Sí, rompes un poco con el principio de responsabilidad única de SOLID, pero para este caso práctico tiene sentido: la pantalla de creación y la de listado comparten dominio.

¿Qué es un StateFlow en Android? Es un flujo observable que mantiene siempre un valor actual y emite cambios a sus suscriptores. Ideal para representar el estado de una pantalla en Compose.

¿Cómo transformar un Flow del repositorio en StateFlow?

Desde el ViewModel expones un preOrderState que parte de preOrderRepository.getPreOrders(). Sobre ese Flow aplicas map y dentro usas result.fold para devolver PreOrderState con isLoading = false en éxito o con isError = true cuando algo falla [02:20].

Luego conviertes ese Flow a StateFlow con stateIn, configurando la estrategia WhileSubscribed (que deja de emitir cinco segundos después de perder suscriptores) y un valor inicial con isLoading = true. Así la vista arranca siempre mostrando el loader y luego se actualiza sola.

¿Cómo agregar acciones de sincronizar y eliminar en el ViewModel?

La lista no es solo lectura. Cada preorden puede borrarse localmente o reintentar su envío al servicio remoto. Para eso defines dos métodos dentro de viewModelScope:

  • onDelete(id: Long) que llama a preOrderRepository.deletePreOrder(id).
  • onSync(id: Long) que llama a preOrderRepository.retrySync(id).

Ambos reciben un Long porque el identificador local de cada preorden en Room es de ese tipo [04:10]. Con esto el ViewModel queda completo: expone estado y dos acciones claras.

¿Cómo se construye la PreOrderScreen con Jetpack Compose?

En la capa de presentación creas una composable llamada PreOrderScreen que recibe un modifier desde el main y el PreOrderViewModel inyectado con hiltViewModel(). Observas el estado con collectAsState() y luego ramificas el render con un when sobre tres casos: isLoading, isError y el caso de datos.

Para los dos primeros puedes reutilizar los composables del home, lo que evita duplicar código de pantallas vacías o de error. En el else muestras una LazyColumn con padding de 8 dp en start, 16 dp en top, alineación vertical y 12 dp de separación entre ítems.

¿Cuándo usar LazyColumn en Compose? Cuando tu lista puede crecer o desconoces su tamaño. LazyColumn solo compone los elementos visibles, mejorando el rendimiento frente a un Column con scroll.

¿Cómo diseñar el PreOrderItem con estado visual?

Cada ítem es un Row con fillMaxWidth, padding de 8 dp, fondo MaterialTheme.colorScheme.surface y esquinas redondeadas con un shape de 8 dp. Dentro usas un Column con weight(1f) que contiene dos Text: el nombre del cliente con estilo titleLarge y el producto con bodyLarge.

A la derecha colocas un Icon que cambia según el flag de sincronización del item:

  • Si la preorden ya se envió, muestras Icons.Default.CheckCircle en color Green.
  • Si está pendiente, muestras Icons.Default.Warning en color Red.
  • El contentDescription también se condiciona entre enviado y pendiente.

Usar un @Preview con showBackground = true te permite validar el diseño sin correr la app completa, alternando el flag entre true y false para ver ambos estados [09:30].

¿Cómo agregar un menú de acciones con DropdownMenu?

Cuando la preorden todavía no se ha sincronizado, necesitas ofrecer dos acciones: reintentar el envío o eliminarla. La solución es un DropdownMenu que se despliega desde un icono de tres puntos.

Envuelves todo en un Box y usas un IconButton con Icons.Default.MoreVert y la descripción "Más opciones". El estado del menú vive en una variable local con remember:

kotlin var expanded by remember { mutableStateOf(false) }

Al hacer click cambias expanded = true. Dentro del DropdownMenu agregas dos DropdownMenuItem: uno con texto "Sincronizar" y otro con "Eliminar". Cada uno cierra el menú con expanded = false y dispara una lambda que recibe el id: Long de la preorden.

Si la preorden ya está enviada, en lugar del menú muestras un Spacer con width de 48 dp para mantener el alineamiento horizontal del Row.

¿Cómo conectar la PreOrderScreen con las acciones del ViewModel?

El PreOrderItem recibe dos lambdas: onSync: (Long) -> Unit y onDelete: (Long) -> Unit. Desde la LazyColumn las pasas apuntando a los métodos del ViewModel:

  • onSync = { id -> viewModel.onSync(id) }
  • onDelete = { id -> viewModel.onDelete(id) }

Un detalle típico de error: si olvidas conectar estas lambdas, el botón de eliminar no hará nada y verás que el registro persiste [16:45]. Con la conexión correcta, eliminar borra la preorden de Room y la lista se actualiza sola gracias al StateFlow.

Finalmente, en MainScreen invocas PreOrdersScreen(modifier = Modifier.padding(padding)) pasándole el padding del Scaffold. Al correr el proyecto y entrar a la pestaña de preórdenes verás los registros creados antes: los que tienen check ya viajaron al servicio remoto y los que tienen warning esperan sincronización.

Probar el flujo offline confirma el comportamiento: creas una preorden sin internet, la app la guarda en Room, y al reconectarte el botón "Sincronizar" dispara el reintento y el servicio responde con un 200, cambiando el icono a verde.

¿Te animas a aplicar este patrón de estado en otras pantallas de tu app? Cuéntame en los comentarios qué desafíos has tenido al combinar Room con StateFlow en Compose.