Creación de un Detail Screen en Jetpack Compose para Android
Clase 15 de 19 • Curso de Android: Modo Offline con Room y Realm
Resumen
La navegación entre pantallas es un componente esencial en el desarrollo de aplicaciones móviles modernas. En este artículo, exploraremos cómo implementar una vista detallada de órdenes en una aplicación Android utilizando Jetpack Compose, siguiendo patrones de arquitectura MVVM y aprovechando Navigation Compose para gestionar la navegación entre pantallas.
¿Cómo crear un ViewModel para la pantalla de detalles?
El primer paso para implementar nuestra pantalla de detalles es crear un ViewModel dedicado que gestionará el estado y la lógica de negocio. Este componente es fundamental en la arquitectura MVVM (Model-View-ViewModel).
Configuración inicial del ViewModel
Para comenzar, necesitamos crear una clase OrderDetailViewModel
que extienda de ViewModel
:
@HiltViewModel
class OrderDetailViewModel @Inject constructor(
private val orderRepository: OrderRepository,
private val savedStateHandle: SavedStateHandle
) : ViewModel() {
private val orderId: String? = savedStateHandle[ARGUMENT_ORDER_ID]
private val _state = MutableStateFlow(OrderDetailState())
val state = _state.asStateFlow()
init {
viewModelScope.launch {
orderId?.let { id ->
_state.value = _state.value.copy(isLoading = true)
try {
val order = orderRepository.getOrder(id)
_state.value = _state.value.copy(
order = order,
isLoading = false
)
} catch (e: Exception) {
_state.value = _state.value.copy(
isError = true,
isLoading = false
)
}
} ?: run {
_state.value = _state.value.copy(
isError = true,
isLoading = false
)
}
}
}
}
Definición del estado de la pantalla
Es importante definir una clase de estado que represente todos los posibles estados de nuestra pantalla:
data class OrderDetailState(
val order: Order? = null,
val isLoading: Boolean = true,
val isError: Boolean = false
)
Este enfoque nos permite manejar de forma elegante los diferentes estados de la interfaz de usuario: carga, error y éxito (cuando tenemos los datos de la orden).
¿Cómo diseñar la interfaz de usuario para la pantalla de detalles?
Una vez que tenemos nuestro ViewModel configurado, podemos proceder a crear la interfaz de usuario utilizando Jetpack Compose.
Creación de la pantalla principal
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun OrderDetailScreen(
modifier: Modifier = Modifier,
viewModel: OrderDetailViewModel = hiltViewModel(),
onBack: () -> Unit
) {
val state by viewModel.state.collectAsState()
Scaffold(
topBar = {
TopAppBar(
title = { Text("Detalle") },
navigationIcon = {
IconButton(onClick = onBack) {
Icon(
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
contentDescription = "Atrás"
)
}
}
)
}
) { padding ->
Box(
modifier = modifier
.fillMaxSize()
.padding(padding),
contentAlignment = Alignment.Center
) {
when {
state.isLoading -> {
// Mostrar indicador de carga
CircularProgressIndicator()
}
state.isError -> {
// Mostrar mensaje de error
Text("Ha ocurrido un error")
}
else -> {
state.order?.let { order ->
ItemOrder(order = order)
} ?: Text("No hay datos")
}
}
}
}
}
Diseño del componente de detalle de orden
Para mostrar los detalles de la orden, creamos un componente reutilizable:
@Composable
fun ItemOrder(order: Order) {
Column(
modifier = Modifier
.fillMaxSize()
.padding(16.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
// Cargar imagen del producto
val painter = rememberAsyncImagePainter(
model = order.imageUrl
)
Image(
painter = painter,
contentDescription = null,
contentScale = ContentScale.Crop,
modifier = Modifier
.height(200.dp)
.clip(RoundedCornerShape(16.dp))
)
Spacer(modifier = Modifier.height(8.dp))
// Nombre del producto
Text(
text = order.item,
style = MaterialTheme.typography.bodyMedium,
modifier = Modifier.fillMaxWidth()
)
Spacer(modifier = Modifier.height(8.dp))
// Nombre del cliente
Text(
text = "Cliente: ${order.customerName}",
style = MaterialTheme.typography.bodyLarge
)
Spacer(modifier = Modifier.height(8.dp))
// Total de la orden
Text(
text = "Total de la orden: ${order.total}",
style = MaterialTheme.typography.bodyLarge,
color = MaterialTheme.colorScheme.primary
)
}
}
Este componente muestra de manera organizada la imagen del producto, el nombre del producto, el nombre del cliente y el total de la orden.
¿Cómo configurar la navegación entre pantallas?
Para completar nuestra implementación, necesitamos configurar la navegación entre la pantalla de listado y la pantalla de detalles.
Definición de rutas de navegación
Primero, definimos una nueva ruta para nuestra pantalla de detalles:
sealed class Screen(
val route: String,
val title: String,
val icon: ImageVector
) {
// Otras pantallas...
object DetailOrder : Screen(
route = "detail/{$ARGUMENT_ORDER_ID}",
title = "Detalle",
icon = Icons.Default.Home
) {
fun getDetailRoute(orderId: String): String {
return "detail/$orderId"
}
}
companion object {
const val ARGUMENT_ORDER_ID = "orderId"
}
}
Configuración del NavHost
Luego, actualizamos nuestro NavHost para incluir la nueva pantalla:
NavHost(
navController = navController,
startDestination = Screen.Home.route
) {
// Otras rutas...
composable(
route = Screen.DetailOrder.route,
arguments = listOf(
navArgument(Screen.ARGUMENT_ORDER_ID) {
type = NavType.StringType
}
)
) {
OrderDetailScreen(
modifier = Modifier.padding(paddingValues),
onBack = { navController.popBackStack() }
)
}
}
Implementación de la navegación desde el listado
Finalmente, modificamos nuestra pantalla de listado para navegar a la pantalla de detalles cuando se hace clic en un elemento:
@Composable
fun HomeScreen(
modifier: Modifier = Modifier,
viewModel: HomeViewModel = hiltViewModel(),
navigateToDetail: (String) -> Unit = {}
) {
// Código existente...
LazyColumn {
items(state.orders) { order ->
ItemView(
order = order,
navigateToDetail = navigateToDetail
)
}
}
}
@Composable
fun ItemView(
order: Order,
navigateToDetail: (String) -> Unit
) {
Card(
modifier = Modifier
.fillMaxWidth()
.padding(8.dp)
.clickable { navigateToDetail(order.id) }
) {
// Contenido del item...
}
}
En el MainScreen
, implementamos la navegación:
HomeScreen(
navigateToDetail = { orderId ->
navController.navigate(Screen.DetailOrder.getDetailRoute(orderId))
}
)
Con esta implementación, hemos creado un flujo completo que permite a los usuarios ver una lista de órdenes y navegar al detalle de cada una, siguiendo las mejores prácticas de arquitectura y diseño en Android con Jetpack Compose.
La combinación de ViewModel para la lógica de negocio, Composables para la interfaz de usuario y Navigation Compose para la navegación nos proporciona una solución robusta y mantenible para nuestra aplicación. ¿Has implementado navegación en tus proyectos con Compose? Comparte tu experiencia en los comentarios.