Listar, visualizar y gestionar preórdenes en Android Studio

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

Resumen

La gestión de preórdenes en aplicaciones Android es fundamental para ofrecer una experiencia de usuario fluida, especialmente cuando se trabaja con funcionalidades offline. Dominar la visualización, eliminación y sincronización de datos locales con servicios remotos te permitirá crear aplicaciones robustas que funcionen incluso sin conexión a internet.

¿Cómo implementar la visualización de preórdenes en Android?

Para visualizar las preórdenes creadas previamente, necesitamos reutilizar el ViewModel existente y crear una nueva pantalla. Aunque esto podría considerarse una ligera desviación del principio de responsabilidad única de SOLID, es una solución práctica para nuestro caso.

Creación del estado de la UI

El primer paso es definir una clase de estado para nuestra interfaz:

data class PreOrderState(
    val isLoading: Boolean = false,
    val data: List<PreOrder> = emptyList(),
    val isError: Boolean = false
)

A continuación, creamos un StateFlow para manejar el estado de la vista:

val preOrderState: StateFlow<PreOrderState> = preOrderRepository.getPreOrders()
    .map { result ->
        result.fold(
            onSuccess = { data -> 
                PreOrderState(isLoading = false, data = data, isError = false) 
            },
            onFailure = { 
                PreOrderState(isLoading = false, data = emptyList(), isError = true) 
            }
        )
    }
    .stateIn(
        viewModelScope,
        SharingStarted.WhileSubscribed(5000),
        PreOrderState(isLoading = true)
    )

Implementación de funciones para eliminar y sincronizar

Además de visualizar las preórdenes, necesitamos funcionalidades para eliminarlas localmente o sincronizarlas con el servidor:

fun delete(id: Long) {
    preOrderRepository.delete(id)
}

fun onSync(id: Long) {
    preOrderRepository.onRetrySync(id)
}

¿Cómo diseñar la interfaz de usuario para mostrar las preórdenes?

Una vez que tenemos la lógica de negocio implementada, procedemos a crear la interfaz de usuario utilizando Jetpack Compose.

Creación de la pantalla principal

@Composable
fun PreOrdersScreen(
    modifier: Modifier = Modifier,
    viewModel: PreOrderViewModel = hiltViewModel()
) {
    val state by viewModel.preOrderState.collectAsState()
    
    when {
        state.isLoading -> {
            // Mostrar indicador de carga
            Box(
                modifier = Modifier.fillMaxSize(),
                contentAlignment = Alignment.Center
            ) {
                CircularProgressIndicator()
            }
        }
        state.isError -> {
            // Mostrar mensaje de error
            Box(
                modifier = Modifier.fillMaxSize(),
                contentAlignment = Alignment.Center
            ) {
                Text("Error al cargar las preórdenes")
            }
        }
        else -> {
            LazyColumn(
                modifier = modifier
                    .fillMaxSize()
                    .padding(start = 8.dp, top = 16.dp),
                verticalArrangement = Arrangement.spacedBy(12.dp)
            ) {
                items(state.data) { item ->
                    PreOrderItem(
                        item = item,
                        onSync = { id -> viewModel.onSync(id) },
                        onDelete = { id -> viewModel.delete(id) }
                    )
                }
            }
        }
    }
}

Diseño del elemento individual de preorden

Para cada preorden, creamos un componente reutilizable que muestra la información relevante y proporciona opciones para sincronizar o eliminar:

@Composable
fun PreOrderItem(
    item: PreOrder,
    onSync: (Long) -> Unit,
    onDelete: (Long) -> Unit
) {
    var expanded by remember { mutableStateOf(false) }
    
    Row(
        modifier = Modifier
            .fillMaxWidth()
            .padding(8.dp)
            .background(
                MaterialTheme.colorScheme.surface,
                shape = RoundedCornerShape(8.dp)
            ),
        verticalAlignment = Alignment.CenterVertically,
        horizontalArrangement = Arrangement.SpaceBetween
    ) {
        Column(
            modifier = Modifier.weight(1f)
        ) {
            Text(
                text = item.customerName,
                style = MaterialTheme.typography.titleLarge
            )
            Text(
                text = item.product,
                style = MaterialTheme.typography.bodyLarge
            )
        }
        
        Icon(
            modifier = Modifier.size(24.dp),
            imageVector = if (item.isSent) Icons.Default.CheckCircle else Icons.Default.Warning,
            contentDescription = if (item.isSent) "Enviado" else "Pendiente",
            tint = if (item.isSent) MaterialTheme.colorScheme.primary else if (item.isError) Color.Red else Color.Green
        )
        
        if (item.isSent) {
            Spacer(modifier = Modifier.width(48.dp))
        } else {
            Box {
                IconButton(
                    onClick = { expanded = true }
                ) {
                    Icon(
                        imageVector = Icons.Default.MoreVert,
                        contentDescription = "Más opciones"
                    )
                }
                
                DropdownMenu(
                    expanded = expanded,
                    onDismissRequest = { expanded = false }
                ) {
                    DropdownMenuItem(
                        onClick = {
                            onSync(item.id)
                            expanded = false
                        },
                        text = { Text("Sincronizar") }
                    )
                    DropdownMenuItem(
                        onClick = {
                            onDelete(item.id)
                            expanded = false
                        },
                        text = { Text("Eliminar") }
                    )
                }
            }
        }
    }
}

Previsualización del componente

Para facilitar el desarrollo, es útil crear una previsualización del componente:

@Preview(showBackground = true)
@Composable
fun PreviewPreOrderItem() {
    PreOrderItem(
        item = PreOrder(
            id = 1L,
            customerName = "Cliente de prueba",
            product = "Producto de prueba",
            isSent = false
        ),
        onSync = {},
        onDelete = {}
    )
}

¿Cómo integrar la pantalla en la navegación principal?

Finalmente, integramos nuestra nueva pantalla en la navegación principal de la aplicación:

// En MainScreen.kt
PreOrdersScreen(
    modifier = Modifier.padding(paddingValues)
)

Al ejecutar la aplicación, podemos ver las preórdenes creadas anteriormente. Las que tienen un icono de verificación ya han sido sincronizadas con el servidor, mientras que las que muestran un icono de advertencia están almacenadas localmente y pueden ser sincronizadas o eliminadas.

La funcionalidad offline es crucial en aplicaciones móviles modernas, ya que permite a los usuarios seguir trabajando incluso cuando no tienen conexión a internet. Al implementar correctamente la sincronización, podemos ofrecer una experiencia fluida donde los datos se guardan localmente y se envían al servidor cuando hay conexión disponible.

¿Has implementado alguna vez funcionalidades offline en tus aplicaciones? Comparte tu experiencia y los desafíos que encontraste en los comentarios.