Creación de UI en Android: Composables y StateFlow con Hilt

Clase 12 de 19Curso de Android: Modo Offline con Room y Realm

Resumen

La programación en Android con Jetpack Compose representa un cambio paradigmático en el desarrollo de interfaces de usuario. A través de componentes declarativos y una arquitectura moderna, los desarrolladores pueden crear aplicaciones más mantenibles y escalables. En esta guía, exploraremos cómo implementar la capa de presentación en una aplicación Android utilizando ViewModel y Composables, elementos fundamentales para construir interfaces dinámicas y reactivas.

¿Cómo implementar la capa de presentación con ViewModel?

La capa de presentación es responsable de mostrar datos al usuario y manejar las interacciones. Para implementar esta capa correctamente, necesitamos crear un ViewModel que gestione el estado de nuestra aplicación.

Creación del ViewModel

Para comenzar, necesitamos crear un paquete de presentación y nuestro primer ViewModel:

@HiltViewModel
class OrderViewModel @Inject constructor(
    private val orderRepository: OrderRepository
) : ViewModel() {
    
    data class HomeState(
        val isLoading: Boolean = false,
        val isError: Boolean = false,
        val data: List<Order> = emptyList()
    )
    
    val homeState: StateFlow<HomeState> = orderRepository.getOrders()
        .distinctUntilChanged()
        .map { result ->
            result.fold(
                onSuccess = { orders ->
                    HomeState(
                        isLoading = false,
                        isError = false,
                        data = orders
                    )
                },
                onFailure = {
                    HomeState(
                        isLoading = false,
                        isError = true,
                        data = emptyList()
                    )
                }
            )
        }
        .stateIn(
            scope = viewModelScope,
            started = SharingStarted.WhileSubscribed(5000),
            initialValue = HomeState(isLoading = true)
        )
}

En este código:

  1. Usamos la anotación @HiltViewModel para que Hilt pueda inyectar este ViewModel en el grafo de dependencias.

  2. Creamos una clase de datos HomeState que representa el estado de nuestra vista, con tres propiedades:

    • isLoading: indica si los datos están cargando
    • isError: indica si ocurrió un error
    • data: contiene la lista de órdenes
  3. Utilizamos StateFlow para emitir actualizaciones del estado a la UI.

  4. Transformamos el flujo de datos del repositorio usando operadores como distinctUntilChanged y map.

  5. Convertimos el flujo en un StateFlow usando stateIn, especificando:

    • El alcance del ViewModel
    • Cuándo debe estar activo el flujo
    • Un valor inicial (estado de carga)

¿Cómo crear Composables para mostrar datos en la UI?

Una vez que tenemos nuestro ViewModel configurado, necesitamos crear Composables para mostrar los datos en la interfaz de usuario.

Configuración de dependencias

Primero, agreguemos la biblioteca Coil para cargar imágenes:

// En build.gradle del módulo
implementation "io.coil-kt:coil-compose:2.4.0"

Creación del HomeScreen

Ahora, creemos nuestro Composable principal:

@Composable
fun HomeScreen(
    modifier: Modifier = Modifier,
    viewModel: OrderViewModel = hiltViewModel()
) {
    val state by viewModel.homeState.collectAsState()
    
    when {
        state.isLoading -> {
            Column(
                modifier = modifier
                    .fillMaxSize()
                    .background(MaterialTheme.colors.background),
                verticalArrangement = Arrangement.Center,
                horizontalAlignment = Alignment.CenterHorizontally
            ) {
                CircularProgressIndicator(color = MaterialTheme.colors.primary)
            }
        }
        state.isError -> {
            Column(
                modifier = modifier
                    .fillMaxSize()
                    .background(MaterialTheme.colors.background),
                verticalArrangement = Arrangement.Center,
                horizontalAlignment = Alignment.CenterHorizontally
            ) {
                Text(
                    text = "Error de datos",
                    modifier = Modifier.align(Alignment.CenterHorizontally)
                )
            }
        }
        else -> {
            LazyColumn(
                modifier = modifier
                    .padding(start = 8.dp, top = 8.dp)
                    .fillMaxSize(),
                verticalArrangement = Arrangement.spacedBy(12.dp)
            ) {
                items(state.data) { order ->
                    ItemView(order = order)
                }
            }
        }
    }
}

En este Composable:

  1. Obtenemos una referencia al ViewModel usando hiltViewModel()
  2. Recopilamos el estado del ViewModel usando collectAsState()
  3. Utilizamos una estructura when para mostrar diferentes UI según el estado:
    • Un indicador de progreso circular durante la carga
    • Un mensaje de error si algo salió mal
    • Una lista de elementos si todo está bien

Creación del ItemView

Para mostrar cada elemento de la lista, creamos un Composable separado:

@Composable
fun ItemView(order: Order) {
    Row(
        modifier = Modifier.fillMaxWidth(),
        verticalAlignment = Alignment.CenterVertically,
        horizontalArrangement = Arrangement.SpaceBetween
    ) {
        val painter = rememberAsyncImagePainter(
            ImageRequest.Builder(LocalContext.current)
                .data(order.imageUrl)
                .placeholder(R.drawable.progress_indeterminate_horizontal)
                .error(R.drawable.stat_notify_error)
                .build()
        )
        
        Image(
            painter = painter,
            contentDescription = null,
            contentScale = ContentScale.Crop,
            modifier = Modifier
                .size(48.dp)
                .clip(CircleShape)
        )
        
        Column(
            modifier = Modifier
                .weight(1f)
                .padding(start = 16.dp),
            verticalArrangement = Arrangement.Center
        ) {
            Text(
                text = order.customerName,
                style = MaterialTheme.typography.titleLarge,
                maxLines = 1,
                overflow = TextOverflow.Ellipsis
            )
            
            Text(
                text = order.item,
                style = MaterialTheme.typography.bodyLarge,
                maxLines = 1,
                overflow = TextOverflow.Ellipsis
            )
        }
        
        Icon(
            imageVector = Icons.AutoMirrored.Filled.ArrowForward,
            contentDescription = null,
            modifier = Modifier
                .align(Alignment.CenterVertically)
                .padding(end = 16.dp)
        )
    }
}

En este Composable:

  1. Utilizamos Coil para cargar imágenes de forma eficiente con manejo de estados de carga y error
  2. Mostramos la imagen del producto en forma circular
  3. Mostramos el nombre del cliente y el nombre del producto en una columna
  4. Agregamos un icono de flecha para indicar que el elemento es seleccionable

Previsualización del ItemView

Para facilitar el desarrollo, podemos agregar una previsualización:

@Preview(showBackground = true)
@Composable
fun ItemViewPreview() {
    ItemView(
        order = Order(
            id = "1",
            customerName = "Cliente de ejemplo",
            item = "Producto de ejemplo",
            total = 99.99,
            imageUrl = "https://example.com/image.jpg"
        )
    )
}

Esta previsualización nos permite ver cómo se verá nuestro Composable sin necesidad de ejecutar la aplicación.

¿Cómo funciona la arquitectura de presentación en Android?

La arquitectura de presentación en Android con Jetpack Compose sigue un patrón unidireccional de flujo de datos:

  1. ViewModel: Mantiene el estado de la UI y procesa eventos
  2. StateFlow: Emite actualizaciones de estado a los Composables
  3. Composables: Renderizan la UI basada en el estado actual

Beneficios de esta arquitectura:

  • Separación de responsabilidades: El ViewModel maneja la lógica de negocio, mientras que los Composables se encargan solo de la UI
  • Reactividad: Los cambios en el estado se propagan automáticamente a la UI
  • Testabilidad: Es más fácil probar cada componente de forma aislada
  • Mantenibilidad: El código es más organizado y fácil de entender

La implementación de la capa de presentación con ViewModel y Composables es fundamental para crear aplicaciones Android modernas y robustas. Con estos componentes, podemos construir interfaces de usuario dinámicas que respondan eficientemente a los cambios de datos y proporcionen una experiencia de usuario fluida.

¿Has implementado alguna vez esta arquitectura en tus proyectos? Comparte tu experiencia en los comentarios y cuéntanos qué desafíos has enfrentado al trabajar con Jetpack Compose.