HomeScreen con ViewModel, StateFlow y Coil

Resumen

Construir la capa de presentación en Android con Jetpack Compose implica conectar un ViewModel con composables que reaccionen a estados. Aquí aprendes a crear un ViewModel con Hilt, exponer un StateFlow y renderizar la pantalla home con Coil para imágenes, ideal si trabajas en arquitectura limpia con capas domain y data.

Cómo se estructura el ViewModel con Hilt y StateFlow

La capa de presentación arranca con un paquete presentation y una clase OrderViewModel que extiende ViewModel. Para que Hilt lo reconozca dentro de su grafo de dependencias, agregas la anotación @HiltViewModel y el constructor lleva @Inject con el OrderRepository como parámetro [01:05].

¿Qué hace @HiltViewModel? Registra el ViewModel en el grafo de Hilt para que pueda inyectar dependencias automáticamente, como el repositorio que necesitas para traer las órdenes.

El estado de la vista se modela con un data class llamado HomeState que contiene tres propiedades: isLoading (boolean inicializado en false), isError (boolean en false) y data como una lista vacía de órdenes. Esta separación te permite manejar carga, error y éxito sin mezclar lógica.

Cómo convertir un Flow en StateFlow observable

Dentro del ViewModel defines val homeState: StateFlow<HomeState> que toma el flujo del repositorio y lo transforma. La cadena queda así:

  • getOrders() devuelve un Flow desde el repositorio.
  • Aplicas un filter para evitar emisiones repetidas si los datos no cambian.
  • Usas map para procesar el Result y manejarlo con fold.
  • En el caso de éxito: isLoading = false, isError = false, data = listaDeOrdenes.
  • En el caso de fallo: isError = true, isLoading = false, data = emptyList().

Finalmente, conviertes el Flow en StateFlow con stateIn, pasando el viewModelScope, una política de inicio que detiene emisiones cinco segundos después de que nadie observe, y un valor inicial con isLoading = true [03:40].

Cómo se construye la pantalla home con Jetpack Compose

La pantalla principal vive en un composable llamado HomeScreen anotado con @Composable. Recibe dos parámetros: un Modifier y el OrderViewModel inyectado mediante hiltViewModel(). Para escuchar el estado, usas collectAsState() sobre homeState, lo que te entrega un State<HomeState> reactivo.

El renderizado se resuelve con un when sobre las tres condiciones del estado:

  1. Loading: una Column con fillMaxSize, fondo de MaterialTheme.colorScheme.background, alineación centrada vertical y horizontal, y un CircularProgressIndicator con color primario.
  2. Error: misma estructura de columna, pero con un Text que avisa del fallo, alineado horizontalmente con Modifier.align.
  3. Éxito: una LazyColumn que itera sobre state.value.data y dibuja cada orden.

¿Para qué sirve collectAsState en Compose? Suscribe el composable al StateFlow del ViewModel y recompone la UI cada vez que el estado cambia, sin que tengas que gestionar listeners manualmente.

Cómo cargar imágenes remotas con Coil

Para mostrar la foto de cada orden necesitas la librería Coil 2.4, agregada al build.gradle del módulo app con la implementación específica para Compose [05:30]. Coil maneja descarga, caché y errores de red sin que tengas que escribir esa lógica.

Dentro del componente ItemView, defines un painter con rememberAsyncImagePainter, al que le pasas un ImageRequest.Builder configurado con:

  • El contexto local obtenido con LocalContext.current.
  • El data que es la URL de la imagen.
  • Un placeholder con un drawable de barra de progreso indeterminada.
  • Un fallback de error con un drawable como StateNotifyError.

Luego usas el composable Image con contentScale = ContentScale.Crop, tamaño de 48 dp y Modifier.clip(CircleShape) para que se vea redonda.

Cómo diseñar el ItemView con preview en tiempo real

Cada orden se renderiza en un composable ItemView que recibe un objeto Order del Domain. Recuerda que la capa de presentación solo se comunica con Domain, nunca directamente con data. La estructura es un Row con fillMaxWidth, verticalAlignment = Alignment.CenterVertically y horizontalArrangement con espacios.

Dentro del row colocas:

  • La imagen circular cargada con Coil.
  • Una Column con Modifier.weight(1f) y padding de 16 dp que contiene dos Text: el nombre del cliente con estilo titleLarge, maxLines = 1 y overflow = TextOverflow.Ellipsis, más el nombre del producto con estilo bodyLarge.
  • Un Icon con Icons.AutoMirrored.Filled.ArrowForward, alineado al centro vertical y con padding de 16 dp al final.

Por qué usar @Preview acelera el desarrollo en Compose

Compose te permite anotar una función con @Preview(showBackground = true) para visualizar el composable sin ejecutar la app. Creas una función como ItemViewPreview que llama a ItemView con un Order hardcodeado (nombre de cliente, producto, total, URL de imagen) y Android Studio renderiza el resultado en el panel lateral [09:15].

¿Qué errores comunes aparecen al inyectar ViewModels con Hilt? Suelen faltar dos cosas: la anotación @AndroidEntryPoint en MainActivity y el @Inject constructor en el ViewModel. Sin ambas, el grafo de dependencias no se construye.

La LazyColumn final lleva un padding de 8 dp, fillMaxSize y un verticalArrangement con espacio de 12 dp entre ítems para que la lista respire. Con esto ya tienes la pantalla lista para mostrar órdenes traídas desde la base de datos local o remota.

¿Te animas a replicar este flujo en tu propio proyecto y compartir cómo te fue con la inyección de Hilt?