Creación de UI en Android: Composables y StateFlow con Hilt
Clase 12 de 19 • Curso 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:
-
Usamos la anotación
@HiltViewModel
para que Hilt pueda inyectar este ViewModel en el grafo de dependencias. -
Creamos una clase de datos
HomeState
que representa el estado de nuestra vista, con tres propiedades:isLoading
: indica si los datos están cargandoisError
: indica si ocurrió un errordata
: contiene la lista de órdenes
-
Utilizamos
StateFlow
para emitir actualizaciones del estado a la UI. -
Transformamos el flujo de datos del repositorio usando operadores como
distinctUntilChanged
ymap
. -
Convertimos el flujo en un
StateFlow
usandostateIn
, 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:
- Obtenemos una referencia al ViewModel usando
hiltViewModel()
- Recopilamos el estado del ViewModel usando
collectAsState()
- 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:
- Utilizamos Coil para cargar imágenes de forma eficiente con manejo de estados de carga y error
- Mostramos la imagen del producto en forma circular
- Mostramos el nombre del cliente y el nombre del producto en una columna
- 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:
- ViewModel: Mantiene el estado de la UI y procesa eventos
- StateFlow: Emite actualizaciones de estado a los Composables
- 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.