Formulario de preórdenes con SharedFlow en Compose

Resumen

Construir un formulario en Jetpack Compose que guarde datos en local y los envíe al servidor parece complejo, pero con un ViewModel bien estructurado y Hilt como inyector de dependencias se vuelve un flujo claro. Aquí verás cómo armar una pantalla de creación de pre-orders con dos campos, validación de botón y notificaciones por eventos.

Cómo se estructura el ViewModel para una pantalla de creación en Compose

El punto de partida es un PreOrderViewModel dentro de la capa presentation. La anotación @HiltViewModel permite que Hilt lo inyecte automáticamente, y el @Inject constructor recibe el PreOrderRepository que conecta con la lógica de almacenamiento [01:10].

A diferencia de un OrderViewModel que expone un state con la última emisión, aquí necesitas notificar acciones puntuales: éxito o error al guardar. Por eso se usa un MutableSharedFlow en lugar de un StateFlow.

¿Cuál es la diferencia entre StateFlow y SharedFlow? El StateFlow guarda y emite la última actualización, ideal para estados de UI. El SharedFlow no almacena por defecto el último valor y se usa para notificaciones puntuales como toasts o navegación.

Cómo modelar los eventos con una sealed class

Una sealed class llamada CreateEvent agrupa los posibles resultados con dos data object:

  • Success: la pre-order se guardó local y remotamente.
  • Error: la pre-order quedó local pero el envío al servidor falló.

El MutableSharedFlow<CreateEvent> se expone con private set para que solo el ViewModel pueda emitir. La función onSavePreOrder(customerName: String, product: String) corre dentro de viewModelScope.launch, ya que savePreOrder del repositorio es una función suspend [03:20].

Cómo se construye la composable CreateScreen paso a paso

La función CreateScreen lleva la anotación @Composable y recibe el PreOrderViewModel por parámetro. Hilt provee la utilidad hiltViewModel() para inyectarlo directamente desde el sistema de navegación.

Dentro de la composable se declaran tres variables con remember y rememberSaveable para sobrevivir a cambios de configuración:

  • productName: string vacío inicial, almacena el nombre del producto.
  • customerName: string vacío inicial, guarda el nombre del cliente.
  • isButtonEnabled: boolean que activa el botón solo si ambos campos tienen contenido.

kotlin var productName by rememberSaveable { mutableStateOf("") } var customerName by rememberSaveable { mutableStateOf("") } var isButtonEnabled by rememberSaveable { mutableStateOf(false) }

Cómo observar eventos del ViewModel desde la UI

Un LaunchedEffect(Unit) se encarga de coleccionar el eventFlow con collectLatest. Según el evento recibido, se dispara un Toast distinto usando LocalContext.current [05:40]:

  • En Success: "Preorden guardada y enviada correctamente al servidor".
  • En Error: "Preorden guardada localmente, pero el envío al servidor falló".

Este patrón mantiene la UI desacoplada del resultado, y permite que el ViewModel notifique sin guardar estado innecesario.

¿Por qué usar LaunchedEffect con SharedFlow? Porque permite recolectar emisiones únicas dentro del ciclo de vida de la composable sin perder eventos durante recomposiciones.

Cómo organizar el layout del formulario con Column y modifiers

El contenedor principal es un Column con Modifier.padding(16.dp).fillMaxSize(), alineado vertical y horizontalmente al centro. Dentro se apilan los siguientes elementos:

  1. Una Image con painterResource(R.drawable.top_image), padding inferior de 24.dp, fillMaxWidth, height(280.dp), esquinas redondeadas con clip de 8.dp y contentScale = ContentScale.Crop.
  2. Un TextField para el nombre del producto, con label "Nombre del producto" y modifier que ocupa todo el ancho.
  3. Un Spacer de 16.dp como separación.
  4. Un segundo TextField para el nombre del cliente con su propio label.
  5. Un Spacer de 24.dp antes del botón.
  6. Un Button con enabled = isButtonEnabled, fillMaxWidth y texto "Guardar pre-order".

En cada onValueChange se actualiza la variable correspondiente y se recalcula isButtonEnabled validando que ambos campos sean distintos de vacío [09:15].

Cómo limpiar el formulario después de guardar

Una mejora útil dentro del collectLatest es resetear los campos cuando llega un evento. Al finalizar el guardado se asigna productName = "", customerName = "" e isButtonEnabled = false. Así el usuario ve el formulario limpio y listo para una nueva pre-order [13:50].

Cómo conectar la pantalla con la navegación principal

El último paso es ir al MainScreen, ubicar la ruta create dentro del NavHost y reemplazar el placeholder por la llamada a CreateScreen(). Al compilar el proyecto, Android Studio despliega el formulario en el emulador con sus dos inputs y el botón inicialmente desactivado.

En la prueba con conexión activa, al ingresar "Control PS5 Room" como producto y "Julián" como cliente, el botón se habilita, el guardado funciona y el toast confirma el envío al servidor. En modo avión con "iPad Air Room" para "Carlos", la consola muestra que la API no respondió y el toast indica que solo se guardó localmente.

¿Qué patrón usas tú para notificar resultados desde un ViewModel a tu composable? Cuéntalo en los comentarios.