Desarrollo Móvil:Flutter + OpenAPI
Guía completa de aprendizaje para construir aplicaciones móviles profesionales conFlutter 3.x,Dart 3.x,OpenAPI GeneratoryRiverpod, consumiendo microservicios REST con documentación OpenAPI.
@riverpod
class ProductoController extends _$ProductoController {
@override
Future<List<Producto>> build() async {
final api = ref.read(productoApiProvider);
return api.listarProductos();
}
Future<void> crear(ProductoCreate dto) async {
final api = ref.read(productoApiProvider);
await api.crearProducto(dto);
await ref.invalidateSelf();
}
}
Identificación de la Guía
Información oficial del programa de formación
| Programa | Análisis y Desarrollo de Software (ADSO) |
| Nivel | Técnico / Tecnólogo |
| Competencia | Desarrollar la solución de software de acuerdo con el diseño y metodologías de desarrollo establecidas |
| Resultado | Construir aplicaciones móviles multiplataforma conectadas a microservicios REST usando Flutter y OpenAPI |
| Duración | 40 horas (20 teoría + 20 práctica) |
| Tecnologías | Flutter 3.xDart 3.xOpenAPIRiverpodDioGoRouterPostgreSQLDockerFastAPICoolify |
| Metodología | Aprendizaje basado en proyectos - API First → Cliente OpenAPI → App Flutter |
Quiz Rápido
Verifica tu comprensión de la guía
Presentación
Introducción al desarrollo móvil multiplataforma con Flutter
¿Por qué Flutter?
Flutter es el framework de UI multiplataforma de Google que te permite construir aplicaciones nativas para Android, iOS, Web y escritorio desde un solo código base. Sus ventajas:
- Hot Reload:Cambios reflejados en milisegundos
- Rendimiento nativo:Compilado a código ARM nativo
- Widgets personalizados:UI completamente personalizable
- Una sola base de código:3 plataformas con el mismo código
¿Por qué OpenAPI?
OpenAPI es el estándar industrial para documentar APIs REST. Al integrarlo con Flutter:
- API First:El contrato define la comunicación
- Generación automática:Cliente Dart generado desde la spec
- Tipado seguro:DTOs generados con tipos de Dart
- Documentación viva:Swagger UI interactivo
El flujo API First consiste en: diseñar la API con OpenAPI → generar el cliente Dart → consumir desde Flutter.
¿Por qué Microservicios?
La arquitectura de microservicios permite escalar el desarrollo de forma independiente:
- Desacoplamiento:Cada servicio es independiente
- Escalabilidad:Escalar solo lo necesario
- Tecnología heterogénea:Diferentes stacks por servicio
- Despliegue independiente:Cada servicio se despliega solo
Flutter se convierte en el frontend universal que consume estos microservicios, proporcionando una experiencia unificada al usuario.
Arquitectura de la Solución
Flutter App
Frontend multiplataforma
Riverpod + DioOpenAPI Spec
Contrato API REST
openapi.yamlMicroservicio
Backend desacoplado
FastAPI + PostgreSQLObjetivos de Aprendizaje
Dominar Flutter
Construir interfaces nativas con widgets, navegación y estado
Consumir APIs REST
Integrar servicios web con Dio y OpenAPI Generator
Implementar Riverpod
Manejar estado global con Riverpod de forma reactiva
Desplegar con Docker
Contenerizar y desplegar apps en entornos de producción
Reflexión Inicial
El problema del código espagueti en apps móviles y cómo Flutter lo soluciona
Caso de Estudio: Apps sin Arquitectura
Imagina que heredas una app móvil hecha con WebViews, sin separación de capas, sin manejo de estado y con llamadas HTTP dispersas por toda la UI. Al abrir el código encuentras algo así:
class ProductosScreen extends StatefulWidget {
@override
State<ProductosScreen> createState() => _ProductosScreenState();
}
class _ProductosScreenState extends State<ProductosScreen> {
List? productos;
String? error;
bool cargando = false;
@override
void initState() {
super.initState();
cargarProductos();
}
Future<void> cargarProductos() async {
setState(() => cargando = true);
try {
final http.Client client = http.Client();
final response = await client.get(
Uri.parse('https://api.ejemplo.com/productos'),
);
if (response.statusCode == 200) {
final List data = json.decode(response.body);
setState(() {
productos = data;
cargando = false;
});
}
} catch (e) {
setState(() {
error = e.toString();
cargando = false;
});
}
}
@override
Widget build(BuildContext context) {
if (cargando) return CircularProgressIndicator();
if (error != null) return Text('Error: $error');
return ListView.builder(
itemCount: productos?.length ?? 0,
itemBuilder: (ctx, i) => ListTile(
title: Text(productos![i]['nombre'] ?? ''),
),
);
}
}
Problemas de Calidad
- URLs hardcodeadas en la UI
- Manejo de estado manual con setState
- Tipado inseguro (List dinámica)
- Sin manejo de errores consistente
Problemas de Mantenibilidad
- Lógica de negocio mezclada con UI
- Sin inyección de dependencias
- Difícil de probar unitariamente
- Código duplicado en cada pantalla
Problemas de Escalabilidad
- Sin separación de capas (UI + datos + red)
- Sin caché de respuestas
- Sin interceptors para tokens
- Sin tipado de respuestas API
Preguntas de Reflexión
¿Qué problemas ves en tener URLs de API hardcodeadas en los widgets?
Si necesitas agregar autenticación a todas las llamadas HTTP, ¿cuántos archivos tendrías que modificar?
¿Cómo harías para probar la lógica de carga de productos sin hacer llamadas reales a la API?
¿Qué pasa si el backend cambia el nombre de un campo? ¿Cómo detectas el error?
¿Cómo soluciona Riverpod + OpenAPI Generator todos estos problemas?
Stack Tecnológico
Herramientas y frameworks que usarás en este módulo
Flutter 3.x
Framework de UI multiplataforma de Google. Usa Dart como lenguaje y compila a código nativo ARM para Android e iOS, y a JavaScript/WebAssembly para la web.
- Widgets Material Design y Cupertino
- Hot Reload en tiempo real
- Rendering propio (Skia/Impeller)
- Renderización a 60/120 FPS
Dart 3.x
Lenguaje moderno, tipado y compilado. Soporta programación orientada a objetos, funcional y reactiva.
- Null safety incorporado
- Records y pattern matching
- Async/await nativo
- Compilación AOT y JIT
OpenAPI Generator
Herramienta que genera automáticamente clientes API, modelos DTO y documentación a partir de un archivo de especificación OpenAPI (YAML/JSON).
- Genera modelos tipados en Dart
- Cliente HTTP con Dio
- Soporte para tokens JWT
- Integración con build_runner
Riverpod
Manejo de estado reactivo para Flutter. Heredero de Provider, pero más seguro, testable y flexible.
- Inyección de dependencias nativa
- Providers auto-disposables
- Facilita el testing con override
- Sin dependencia de BuildContext
Dio
Cliente HTTP para Dart con interceptors, timeouts, cancelación y Transformers.
- Interceptors para tokens y logging
- Timeouts configurables
- Cancelación de peticiones
- Parseo automático de JSON
GoRouter
Enrutador declarativo para Flutter con soporte para rutas protegidas, redirecciones y deep linking.
- Rutas tipadas y anidadas
- Redirect condicional (auth)
- Deep links y parámetros URL
- Animaciones entre rutas
1. Instalación del Entorno Flutter
Prepara tu máquina para desarrollar con Flutter y Dart
Instalar Flutter SDK
Descarga e instala Flutter SDK en tu sistema operativo
Opción 1: Instalador oficial
# 1. Descarga Flutter desde https://flutter.dev
# 2. Ejecuta el instalador para Windows
# 3. Verifica la instalación:
flutter doctor
Opción 2: Chocolatey (recomendado)
choco install flutter
flutter doctor
Configurar Android Studio
Instala el emulador de Android y las herramientas de desarrollo
Pasos para Android Studio
# Aceptar licencias de Android
flutter doctor --android-licenses
# Verificar que Android SDK está detectado
flutter doctor -v
Variables de entorno recomendadas
# Agregar al PATH (Windows: variables de entorno del sistema)
export ANDROID_HOME=$HOME/AppData/Local/Android/Sdk
export PATH=$PATH:$ANDROID_HOME/platform-tools
export PATH=$PATH:$ANDROID_HOME/emulator
export PATH=$PATH:$ANDROID_HOME/cmdline-tools/latest/bin
VS Code y Extensiones
Configura tu editor para productividad máxima
Extensiones esenciales
- Flutter — Soporte oficial de Dart y Flutter
- Dart — Análisis de código, autocompletado
- Error Lens — Errores inline en el editor
- Pubspec Assist — Agregar paquetes fácilmente
- Awesome Flutter Snippets — Atajos de código
Verificar con flutter doctor
Comando mágico que verifica todo tu entorno
flutter doctor
# Deberías ver todas las casillas en verde ✓
# ✔ Flutter SDK
# ✔ Android Toolchain
# ✔ Chrome (para web)
# ✔ Android Studio
# ✔ VS Code
Crear primer proyecto
¡Tu primera app Flutter funcionando!
flutter create mi_app
cd mi_app
flutter run
2. Fundamentos de Dart
Variables, tipos, funciones y async — la base de todo en Flutter
Variables y Tipos
Dart 3 tiene null safety, inferencia de tipos y tipado fuerte.
// --- Tipos básicos con null safety ---
int edad = 21;
double precio = 19.99;
String nombre = 'Flutter';
bool activo = true;
// --- Nullable (?) vs non-nullable ---
String? apodo; // Puede ser null
int cantidad = 0; // Nunca será null
// --- Type inference: var, final, const ---
var mensaje = 'Hola Dart'; // String inferido
final url = 'api.google.com'; // Inmutable en runtime
const pi = 3.1416; // Constante en compilación
// --- Records (Dart 3+) ---
(String, int) usuario = ('Ana', 25);
({String nombre, int edad}) persona = (
nombre: 'Luis',
edad: 30,
);
Funciones
Dart soporta funciones de primera clase, parámetros opcionales y expresiones lambda.
// --- Función con parámetros posicionales ---
double sumar(double a, double b) => a + b;
// --- Parámetros nombrados (required / opcional) ---
void saludar({required String nombre, String? apellido}) {
print('Hola, $nombre ${apellido ?? ''}');
}
// --- Función como parámetro (callback) ---
void procesar(List<int> items, void Function(int) cb) {
for (final item in items) cb(item);
}
// --- Lambda / closure ---
final duplicar = (int x) => x * 2;
// --- Pattern matching (Dart 3+) ---
String describir(dynamic valor) => switch (valor) {
int v => 'Entero: $v',
String s => 'Texto: $s',
_ => 'Desconocido',
};
Clases y Objetos
Programación orientada a objetos con constructores, herencia y mixins.
// --- Clase con constructor posicional ---
class Producto {
final int id;
final String nombre;
final double precio;
const Producto(this.id, this.nombre, this.precio);
// Factory constructor desde Map
factory Producto.fromJson(Map<String, dynamic> json) =>
Producto(json['id'], json['nombre'], json['precio']);
}
// --- Herencia con super parámetros ---
class ProductoElectronicos extends Producto {
final int garantiaMeses;
ProductoElectronicos(
super.id, super.nombre, super.precio, this.garantiaMeses);
}
Async / Await
Programación asíncrona con Futures, Streams y async/await.
// --- Future con async/await ---
Future<String> obtenerToken() async {
await Future.delayed(Duration(seconds: 1));
return 'token_jwt_123';
}
// --- Llamada secuencial ---
Future<void> cargarDatos() async {
final token = await obtenerToken();
final datos = await httpGet('/productos', token);
print(datos);
}
// --- Streams con async* / yield ---
Stream<int> contador() async* {
for (int i = 0; i < 5; i++) {
await Future.delayed(Duration(seconds: 1));
yield i;
}
}
// --- Manejo de errores con try / catch ---
Future<void> obtenerProductos() async {
try {
final res = await api.listar();
print('Productos: $res');
} on DioException catch (e) {
print('Error HTTP: ${e.response?.statusCode}');
} catch (e) {
print('Error inesperado: $e');
}
}
3. Widgets y UI
StatelessWidget, StatefulWidget, layouts y temas
StatelessWidget
Widget inmutable: su configuración no cambia después de construirse. Ideal para UI pura.
class TarjetaProducto extends StatelessWidget {
final Producto producto;
const TarjetaProducto({super.key, required this.producto});
@override
Widget build(BuildContext context) {
return Card(
child: ListTile(
leading: Icon(Icons.inventory),
title: Text(producto.nombre),
subtitle: Text('\$${producto.precio}'),
trailing: Icon(Icons.arrow_forward_ios),
),
);
}
}
StatefulWidget
Widget mutable que puede cambiar su apariencia con setState.
class ContadorWidget extends StatefulWidget {
const ContadorWidget({super.key});
@override
State<ContadorWidget> createState() => _ContadorWidgetState();
}
class _ContadorWidgetState extends State<ContadorWidget> {
int _contador = 0;
@override
Widget build(BuildContext context) {
return Column(
children: [
Text('Valor: $_contador'),
FilledButton(
onPressed: () {
setState(() => _contador++);
},
child: Text('Incrementar'),
),
],
);
}
}
Widgets de Layout Esenciales
Column y Row
Layouts verticales y horizontales. Los pilares de toda UI en Flutter.
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('Productos'),
SizedBox(height: 8),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text('Nombre'),
Text('Precio'),
],
),
],
)
GridView y ListView
Listas y cuadrículas para mostrar colecciones de datos.
// ListView básico
ListView.builder(
itemCount: productos.length,
itemBuilder: (ctx, i) => ListTile(
title: Text(productos[i].nombre),
),
)
// GridView
GridView.builder(
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
),
itemBuilder: (ctx, i) => TarjetaProducto(productos[i]),
)
5. Consumo de APIs REST
Dio, JSON, manejo de errores e interceptors
Configuración de Dio
Dio es el cliente HTTP más popular en Flutter. Soporta interceptors, timeouts y cancelación.
final apiClientProvider = Provider<Dio>((ref) {
final dio = Dio(BaseOptions(
baseUrl: 'http://localhost:8000/api',
connectTimeout: Duration(seconds: 10),
receiveTimeout: Duration(seconds: 10),
headers: {'Content-Type': 'application/json'},
));
// Interceptor para token JWT
dio.interceptors.add(InterceptorsWrapper(
onRequest: (options, handler) {
final token = ref.read(authProvider).token;
if (token != null) {
options.headers['Authorization'] = 'Bearer $token';
}
handler.next(options);
},
onError: (error, handler) {
if (error.response?.statusCode == 401) {
ref.read(authProvider.notifier).logout();
}
handler.next(error);
},
));
return dio;
});
Operaciones CRUD
Implementa las operaciones básicas con métodos HTTP estándar.
class ProductoApi {
final Dio _dio;
ProductoApi(this._dio);
Future<List<Producto>> listar() async {
final res = await _dio.get('/productos');
return (res.data as List)
.map((j) => Producto.fromJson(j))
.toList();
}
Future<Producto> obtener(int id) async {
final res = await _dio.get('/productos/$id');
return Producto.fromJson(res.data);
}
Future<Producto> crear(ProductoCreate dto) async {
final res = await _dio.post('/productos', data: dto.toJson());
return Producto.fromJson(res.data);
}
Future<Producto> actualizar(int id, ProductoUpdate dto) async {
final res = await _dio.put('/productos/$id', data: dto.toJson());
return Producto.fromJson(res.data);
}
Future<void> eliminar(int id) async {
await _dio.delete('/productos/$id');
}
}
Manejo de Errores
// Clase genérica para respuestas
sealed class ApiResult<T> {
const ApiResult();
}
class Success<T> extends ApiResult<T> {
final T data;
const Success(this.data);
}
class Failure<T> extends ApiResult<T> {
final String mensaje;
final int? statusCode;
const Failure(this.mensaje, [this.statusCode]);
}
// Uso con switch expression
switch (resultado) {
case Success(data: var productos):
mostrarLista(productos);
case Failure(mensaje: var msg):
mostrarError(msg);
}
6. OpenAPI Generator para Dart
Genera clientes tipados desde la especificación OpenAPI
¿Qué es OpenAPI Generator?
OpenAPI Generator toma un archivo de especificación (YAML/JSON) y genera automáticamente:
- Modelos DTO (Data Transfer Objects)
- Cliente API con métodos tipados
- Configuración de autenticación
- Manejo de errores consistente
Esto elimina errores de tipeo, campos faltantes y URLs mal escritas.
Generar el Cliente Dart
Desde la terminal, ejecuta OpenAPI Generator con el template Dart.
# Usando openapi-generator-cli (Java)
openapi-generator generate \
-i openapi.yaml \
-g dart-dio \
-o lib/api \
--additional-properties=pubName=inventario_api
# O usando la imagen Docker
docker run --rm -v ${PWD}:/local openapitools/openapi-generator-cli \
generate -i /local/openapi.yaml \
-g dart-dio -o /local/lib/api
Ejemplo de openapi.yaml
Especificación mínima para un CRUD de productos
openapi: "3.0.3"
info:
title: API de Inventario
version: "1.0.0"
paths:
/productos:
get:
operationId: listarProductos
responses:
"200":
content:
application/json:
schema:
type: array
items:
$ref: "#/components/schemas/Producto"
post:
operationId: crearProducto
requestBody:
content:
application/json:
schema:
$ref: "#/components/schemas/ProductoCreate"
components:
schemas:
Producto:
type: object
properties:
id: {type: integer}
nombre: {type: string}
precio: {type: number}
ProductoCreate:
type: object
properties:
nombre: {type: string}
precio: {type: number}
Usar el Cliente Generado
Código tipado, sin errores de runtime por campos mal escritos
import 'package:inventario_api/inventario_api.dart';
// Provider del API generada
final productoApiProvider = Provider<ProductoApi>((ref) {
final dio = ref.watch(apiClientProvider);
return ProductoApi(dio);
});
// Riverpod controller usando el API generada
@riverpod
class Productos extends _$Productos {
@override
Future<List<Producto>> build() async {
final api = ref.read(productoApiProvider);
return api.listarProductos();
}
Future<void> crear(ProductoCreate dto) async {
await ref.read(productoApiProvider).crearProducto(dto);
ref.invalidateSelf();
}
}
7. Microservicios
Arquitectura, patrones de comunicación y despliegue
Arquitectura de Microservicios
Cada microservicio es una unidad independiente con su propia base de datos y lógica de negocio.
- Servicio de Productos:CRUD de productos e inventario
- Servicio de Usuarios:Autenticación y perfiles
- Servicio de Pedidos:Órdenes y facturación
- API Gateway:Punto de entrada único
Comunicación Síncrona (REST)
Los microservicios se comunican mediante HTTP/REST con documentación OpenAPI.
from fastapi import FastAPI
from pydantic import BaseModel
app = FastAPI(title="Productos Service")
class Producto(BaseModel):
id: int
nombre: str
precio: float
@app.get("/productos")
async def listar():
return [Producto(id=1, nombre="Laptop", precio=999.99)]
@app.get("/productos/{producto_id}")
async def obtener(producto_id: int):
return Producto(id=producto_id, nombre="Laptop", precio=999.99)
Flujo de Comunicación
Flutter App
Cliente multiplataforma
API Gateway
Enrutamiento + Auth
Microservicios
Productos / Usuarios / Pedidos
Cada microservicio tiene su propia DB PostgreSQL, su propio contrato OpenAPI y se despliega independientemente en Coolify.
8. Tutorial Práctico: App Inventario con OpenAPI
Construye una app Flutter completa conectada a un microservicio FastAPI
Crear el proyecto Flutter
Inicializa el proyecto con las dependencias necesarias
flutter create inventario_app
cd inventario_app
code .
Agregar dependencias en pubspec.yaml
dependencies:
flutter:
sdk: flutter
flutter_riverpod: ^2.5.0
riverpod_annotation: ^2.3.0
go_router: ^14.0.0
dio: ^5.4.0
json_annotation: ^4.9.0
dev_dependencies:
build_runner: ^2.4.0
riverpod_generator: ^2.4.0
json_serializable: ^6.8.0
Configurar la API con OpenAPI Generator
Genera el cliente Dart desde la especificación OpenAPI
# Descargar la spec OpenAPI del microservicio
curl http://localhost:8000/openapi.json > openapi.json
# Generar el cliente Dart
openapi-generator generate \
-i openapi.json \
-g dart-dio \
-o lib/api \
--additional-properties=pubName=inventario_api
# Agregar al pubspec.yaml
dependencies:
inventario_api:
path: lib/api
Crear la pantalla de listado
Muestra los productos usando Riverpod + el API generado
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:inventario_api/inventario_api.dart';
final productoApiProvider = Provider((ref) {
final dio = Dio(BaseOptions(baseUrl: 'http://localhost:8000'));
return ProductoApi(dio);
});
@riverpod
class ProductosController extends _$ProductosController {
@override
Future<List<Producto>> build() async {
final api = ref.read(productoApiProvider);
return api.listarProductos();
}
}
class ProductosScreen extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final productos = ref.watch(productosControllerProvider);
return Scaffold(
appBar: AppBar(title: Text('Inventario')),
body: productos.when(
data: (items) => ListView.builder(
itemCount: items.length,
itemBuilder: (_, i) => ListTile(
title: Text(items[i].nombre ?? ''),
subtitle: Text('\$${items[i].precio}'),
),
),
error: (e, _) => Center(child: Text('Error: $e')),
loading: () => Center(child: CircularProgressIndicator()),
),
);
}
}
Probar la app
Ejecuta en emulador o dispositivo físico
# Listar dispositivos disponibles
flutter devices
# Ejecutar en el emulador
flutter run
# Hot reload automático con 'r' en la terminal
Ejercicios de Transferencia
Ejercicios retadores para demostrar tu dominio de Flutter + OpenAPI
Búsqueda en Tiempo Real
Agrega un buscador que filtre productos mientras el usuario escribe, consumiendo el endpoint de búsqueda del microservicio.
Requisitos:
- Campo de texto con debounce de 300ms
- Llamada al endpoint
GET /productos?q=term - Mostrar resultados en un ListView
- Indicador de carga durante la búsqueda
- Manejar errores de red
- Bonus:Filtros por categoría y rango de precios
Pista:UsaTimer? _debouncepara implementar el debounce. Cancela el timer anterior en cadaonChanged.
Autenticación JWT
Implementa login con JWT, almacenamiento seguro del token y renovación automática.
Requisitos:
- Pantalla de login con usuario y contraseña
- Llamada a
POST /auth/login - Almacenar token con
flutter_secure_storage - Interceptor de Dio que agregue el token
- Redirección automática al login si 401
- Bonus:Refresh token automático
Pista:Usadio.interceptors.add(QueuedInterceptorsWrapper)y maneja el 401 enonError.
App Multi-servicio
Conecta tu app Flutter a dos microservicios diferentes (Productos y Pedidos) usando un API Gateway.
Requisitos:
- Configurar dos APIs generadas con OpenAPI
- Pantalla de productos con opción "Agregar al carrito"
- Pantalla de carrito que consuma el servicio de pedidos
- Estado global con Riverpod para el carrito
- Confirmación de pedido con número de orden
- Bonus:Historial de pedidos con scroll infinito
Pista:UsaGateway /api/productosyGateway /api/pedidos. Configura el baseUrl del Dio al gateway.
Evidencias de Aprendizaje
Demuestra que has alcanzado los resultados de aprendizaje
Evidencia de Conocimiento
Cuestionario escrito individual
- Explicar la arquitectura de widgets en Flutter
- Diferencia entre StatelessWidget y StatefulWidget
- Manejo de estado con Riverpod
- Rol de OpenAPI Generator en el flujo API First
- Ventajas de usar Dio vs http.Client
- Flujo completo: Widget → Provider → API → Microservicio
- GoRouter y rutas protegidas
- Arquitectura de microservicios vs monolito
Evidencia de Desempeño
Proyecto práctico individual
- App Flutter funcional conectada a un microservicio
- Cliente generado con OpenAPI Generator
- Mínimo 3 pantallas con GoRouter
- Manejo de estado con Riverpod
- Autenticación JWT implementada
- Operaciones CRUD completas
- Manejo de errores y carga
Evidencia de Producto
Proyecto colaborativo grupal
- Repositorio GitHub con README y documentación
- Mínimo 2 microservicios desplegados en Coolify
- API Gateway configurado
- OpenAPI spec publicada y versionada
- App Flutter publicada en TestFlight/APK
- Demo funcional con datos reales
- Presentación técnica del proyecto