Este artículo profundiza en la fase de refinamiento de un sistema de gestión de estacionamiento inteligente comercial, desarrollado con Flutter y OpenHarmony. El objetivo es mejorar la experiencia del usuario, asegurar la conformidad regulatoria y preparar el sistema para el despliegue.
1. Recordatorios de Tiempo Límite de Estacionamiento
Para simular escenarios de la vida real, se implementa una funcionalidad que alerta a los usuarios sobre el tiempo de estacionamiento excedido. Si el tiempo de estacionamiento supera un umbral predefinido (por ejemplo, 24 horas), se muestra una ventana emergente notificando sobre cargos adicionales y ofreciendo una opción para proceder al pago.
// Controlador para verificar si el tiempo de estacionamiento ha excedido el límite
void verificarTiempoExcedido(int horasEstacionamiento) {
if (horasEstacionamiento > 24) {
double cargoAdicional = (horasEstacionamiento - 24) * 2.0; // Supongamos una tarifa de 2 por hora extra
mostrarDialogoExcesoTiempo(cargoAdicional);
}
}
// Muestra la ventana emergente de notificación de tiempo excedido
void mostrarDialogoExcesoTiempo(double cargoAdicional) {
Get.dialog(
AlertDialog(
title: const Text("Recordatorio de Tiempo Límite de Estacionamiento"),
content: Text("Ha estado estacionado por más de 24 horas. Cargo adicional por tiempo excedido: ${cargoAdicional.toStringAsFixed(2)}"),
actions: [
TextButton(onPressed: () => Get.back(), child: const Text("Posponer")),
TextButton(onPressed: () => Get.toNamed('/ruta/a/pago'), child: const Text("Pagar Ahora"))
],
),
);
}
2. Controlador de Tema Global
Se introduce un controlador de tema utilizando GetX para gestionar el estado del tema (modo oscuro/claro). Esto permite un cambio dinámico de la apariencia de toda la aplicación con una sola acción, sin necesidad de modificar cada pantalla individualmente.
import 'package:get/get.dart';
class ControlTema extends GetxController {
static ControlTema get instancia => Get.find();
final RxBool esModoOscuro = false.obs;
void alternarTema() {
esModoOscuro.value = !esModoOscuro.value;
}
}
3. Configuración de Temas Oscuro y Claro
Se definen dos conjuntos de estilos de tema: uno para el modo claro (con una paleta azul-grisácea) y otro para el modo oscuro (con tonos grisáceos oscuros). Estos se aplican dinámicamente en la configuración principal de la aplicación (GetMaterialApp) basándose en el estado del ControlTema.
import 'package:flutter/material.dart';
ThemeData obtenerTemaClaro() {
return ThemeData(
primarySwatch: Colors.blueGrey,
scaffoldBackgroundColor: Colors.white,
// ... otros estilos del tema claro
);
}
ThemeData obtenerTemaOscuro() {
return ThemeData(
primarySwatch: Colors.grey,
scaffoldBackgroundColor: const Color(0xFF121212),
// ... otros estilos del tema oscuro
);
}
En main.dart o en el widget raíz de la aplicación:
GetMaterialApp(
theme: ControlTema.instancia.esModoOscuro.value ? obtenerTemaOscuro() : obtenerTemaClaro(),
// ... otras configuraciones de la app
)
4. Interrruptor de Tema en la Interfaz de Usuario
Se añade un control SwitchListTile en la sección de configuración o perfil del usuario. Este interruptor está enlazado al ControlTema, permitiendo a los usuarios alternar fácilmente entre los modos claro y oscuro.
Obx(() => SwitchListTile(
title: const Text("Modo Oscuro"),
value: ControlTema.instancia.esModoOscuro.value,
onChanged: (valor) => ControlTema.instancia.alternarTema(),
))
5. Página de Inicio de Sesión
Se diseña una página de inicio de sesión con una estética limpia y profesional. Incluye campos para nombre de usuario y contraseña, una casilla para "Recordar mi sesión" y un botón de inicio de sesión. Se asegura que el teclado se adapte correctamente, especialmente en pantallas más pequeñas de dispositivos HarmonyOS.
import 'package:flutter/material.dart';
import 'package:get/get.dart';
class PaginaLogin extends StatelessWidget {
PaginaLogin({super.key});
final TextEditingController controladorUsuario = TextEditingController();
final TextEditingController controladorPassword = TextEditingController();
final RxBool recordarSesion = false.obs;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text("Inicio de Sesión")),
body: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
children: [
// Campo de usuario
TextField(controller: controladorUsuario, decoration: const InputDecoration(labelText: "Usuario")),
const SizedBox(height: 16),
// Campo de contraseña
TextField(controller: controladorPassword, obscureText: true, decoration: const InputDecoration(labelText: "Contraseña")),
Obx(() => CheckboxListTile(
title: const Text("Recordar mi sesión"),
value: recordarSesion.value,
onChanged: (valor) => recordarSesion.value = valor ?? false,
)),
const SizedBox(height: 20),
ElevatedButton(
onPressed: () => iniciarSesion(),
child: const Text("Iniciar Sesión"),
),
// ... otros elementos
],
),
),
);
}
void iniciarSesion() {
// Lógica de inicio de sesión aquí
if (controladorUsuario.text.isNotEmpty && controladorPassword.text.isNotEmpty) {
// Navegar a la página principal o realizar otra acción
Get.offAllNamed('/ruta/a/inicio');
} else {
Get.snackbar("Error", "Por favor, ingrese usuario y contraseña.");
}
}
}
6. Función "Recordar mi sesión"
Para mejorar la experiencia del usuario, se implementa la funcionalidad "Recordar mi sesión". Cuando esta opción está marcada, las credenciales del usuario (usuario y contraseña) se guardan localmente utilizando SharedPreferences. Al iniciar la aplicación posteriormente, estos campos se rellenan automáticamente.
import 'package:shared_preferences/shared_preferences.dart';
Future<void> guardarCredenciales(String usuario, String contrasena) async {
final prefs = await SharedPreferences.getInstance();
await prefs.setString("usuarioGuardado", usuario);
await prefs.setString("contrasenaGuardada", contrasena);
}
Future<void> cargarCredenciales() async {
final prefs = await SharedPreferences.getInstance();
controladorUsuario.text = prefs.getString("usuarioGuardado") ?? "";
controladorPassword.text = prefs.getString("contrasenaGuardada") ?? "";
}
</void></void>
7. Lógica de Autenticación y Permisos
Se añade una lógica básica de validación de credenciales. Tras un inicio de sesión exitoso, el estado de autenticación se almacena globalmente (por ejemplo, en un AuthController), y el usuario es redirigido a la página principal. Se implementan guardias de ruta para restringir el acceso a secciones sensibles (como pedidos o centro de miembros) a usuarios autenticados.
// Ejemplo simplificado en el método iniciarSesion
void iniciarSesion() {
if (controladorUsuario.text == "demo" && controladorPassword.text == "12345") {
// Guardar estado de autenticación
// authController.login(usuario: controladorUsuario.text);
if (recordarSesion.value) {
guardarCredenciales(controladorUsuario.text, controladorPassword.text);
}
Get.offAllNamed('/ruta/a/inicio');
} else {
Get.snackbar("Error", "Credenciales inválidas.");
}
}
8. Ventana Emergente de Acuerdo de Privacidad
Conforme a las directrices de HarmonyOS para la publicación de aplicaciones, se implementa una ventana emergente que solicita el consentimiento del usuario para la política de privacidad en el primer lanzamiento de la aplicación. Si el usuario acepta, la ventana no volverá a aparecer. Si la rechaza, la aplicación se cerrará.
Future<void> verificarAceptacionPrivacidad() async {
final prefs = await SharedPreferences.getInstance();
bool aceptado = prefs.getBool("privacidadAceptada") ?? false;
if (!aceptado) {
Get.dialog(dialogoAcuerdoPrivacidad());
}
}
Widget dialogoAcuerdoPrivacidad() {
return AlertDialog(
title: const Text("Política de Privacidad"),
content: const Text("Esta aplicación requiere acceso a su ubicación y red para funcionar correctamente. ¿Acepta nuestra política de privacidad?"),
actions: [
TextButton(onPressed: () => exit(0), child: const Text("Rechazar")),
TextButton(onPressed: () async {
final prefs = await SharedPreferences.getInstance();
await prefs.setBool("privacidadAceptada", true);
Get.back();
}, child: const Text("Aceptar")),
],
);
}
</void>
9. Refinamiento General de la Interfaz de Usuario (UI)
Se estandarizan elementos de la interfaz de usuario como botones, campos de entrada, tarjetas, espaciado y bordes redondeados. El uso de constantes globales para estilos asegura una apariencia coherente en toda la aplicación, mejorando significativamente la calidad visual.
// Ejemplo de un botón principal con estilo unificado
Widget botonPrincipal(String texto, VoidCallback accion) {
return ElevatedButton(
onPressed: accion,
style: ElevatedButton.styleFrom(
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8.0)), // Radio de borde unificado
padding: const EdgeInsets.symmetric(vertical: 12.0),
),
child: Text(texto),
);
}
10. Configuración de Empaquetado y Firma para HarmonyOS
Se configuran los detalles de firma digital para generar paquetes de aplicación instalables (.hap) para HarmonyOS. Esto permite la instalación y demostración directa en dispositivos físicos, cumpliendo con los requisitos para presentaciones de proyectos o demostraciones en vivo.
// Ejemplo de configuración en el archivo de manifiesto (module.json5)
"signingConfig": {
"path": "./firmas/debug.p12",
"keyAlias": "aliasDebug",
"storePassword": "tuContrasenaSecreta"
}
11. Conclusiones y Próximos Pasos
Este quinto día se centra en optimizar la experiencia del usuario, la interfaz, la seguridad y la preparación para el despliegue. El sistema ahora cumple con los estándares comerciales, incluyendo recordatorios de tiempo límite, temas personalizables, un sistema de inicio de sesión robusto y cumplimiento de normativas de privacidad. El próximo día se dedicará a la encapsulación de componentes reutilizables, la implementación de páginas de configuración y comentarios, y la refactorización general del código.