1
02-frontend.md
hack_23031391_8ff9d8 edited this page 2026-05-23 02:51:55 +00:00
This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

Configurar Flutter (Módulos C + D)

Guía paso a paso para integrar los módulos y correr la app.


1. Clonar el repo o descargar módulos

Si ya está en el repo:

cd hackathon-acapulquitos-boys
git pull origin dev
cd basura_app

Si Persona D entrega su módulo por separado:

# Descomprimir ZIP del módulo D
unzip persona_d_modulo.zip

# Copiar archivos al proyecto principal
cp -r persona_d/lib/core basura_app/lib/
cp -r persona_d/lib/features/recycling_guide basura_app/lib/features/
cp persona_d/assets/recycling_guide.json basura_app/assets/

2. Configurar FVM (recomendado)

cd basura_app

# Instalar la versión acordada por el equipo
fvm install 3.44.0    # o 3.22.0 si bajaron versión
fvm use 3.44.0

# Verificar
fvm flutter --version

Esto crea .fvm/fvm_config.json — debe commitearse al repo.


3. Actualizar pubspec.yaml

Abre basura_app/pubspec.yaml y verifica:

name: basura_app
description: App de notificación de recolección de residuos
publish_to: 'none'
version: 1.0.0+1

environment:
  sdk: '>=3.8.0 <4.0.0'      # Dart 3.8 (Flutter 3.44)
  flutter: '>=3.44.0'        # o '>=3.22.0' si bajaron

dependencies:
  flutter:
    sdk: flutter
  
  # State management
  flutter_riverpod: ^2.6.1
  
  # HTTP
  dio: ^5.4.0
  
  # WebSocket
  web_socket_channel: ^2.4.0
  
  # Routing (Persona C decide cuál usar)
  go_router: ^13.2.0         # Recomendado
  # o usa Navigator tradicional
  
  # Utils
  intl: ^0.19.0              # Formateo de fechas

dev_dependencies:
  flutter_test:
    sdk: flutter
  flutter_lints: ^4.0.0

flutter:
  uses-material-design: true
  assets:
    - assets/recycling_guide.json    # ← JSON del módulo D

4. Instalar dependencias

fvm flutter pub get

Deberías ver:

Running "flutter pub get" in basura_app...
Resolving dependencies... 
Got dependencies!

5. Verificar la estructura de carpetas

tree -L 4 lib/

Debe verse algo así:

lib/
├── core/
│   └── theme/
│       └── app_theme.dart              # Tema compartido (Persona D)
│
├── features/
│   ├── auth/                            # Login (Persona C)
│   │   └── presentation/
│   │       └── screens/
│   │           └── login_screen.dart
│   │
│   ├── eta/                             # Home + WebSocket (Persona C)
│   │   ├── domain/
│   │   ├── data/
│   │   └── presentation/
│   │       ├── screens/
│   │       │   └── eta_home_screen.dart
│   │       └── providers/
│   │           ├── eta_provider.dart
│   │           └── ws_provider.dart
│   │
│   └── recycling_guide/                 # Guía offline (Persona D)
│       ├── domain/
│       ├── data/
│       └── presentation/
│           ├── screens/
│           │   ├── recycling_guide_screen.dart
│           │   └── category_detail_screen.dart
│           └── providers/
│               └── recycling_provider.dart
│
└── main.dart

6. Configurar main.dart

// lib/main.dart
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';

import 'core/theme/app_theme.dart';
import 'features/eta/presentation/screens/eta_home_screen.dart';
import 'features/recycling_guide/presentation/screens/recycling_guide_screen.dart';

void main() {
  runApp(const ProviderScope(child: MyApp()));
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp.router(
      title: 'Basura App',
      theme: AppTheme.lightTheme,  // ← Tema de Persona D
      routerConfig: _router,
      debugShowCheckedModeBanner: false,
    );
  }
}

// Router (Persona C configura las rutas finales)
final _router = GoRouter(
  routes: [
    GoRoute(
      path: '/',
      builder: (_, __) => const ETAHomeScreen(),  // Persona C
    ),
    GoRoute(
      path: '/guia',
      builder: (_, __) => const RecyclingGuideScreen(),  // Persona D
    ),
  ],
);

7. Actualizar la URL del backend

Importante: Por defecto, el backend corre en localhost:8000, pero:

  • Emulador Android: usa 10.0.2.2:8000
  • Emulador iOS: usa localhost:8000
  • Dispositivo físico: usa la IP de tu PC en la red local (ej: 192.168.1.10:8000)

Crear un provider de configuración

// lib/core/config/api_config.dart
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:dio/dio.dart';

// Cambia esto según tu entorno
const _baseUrl = 'http://10.0.2.2:8000';  // Android emulator
// const _baseUrl = 'http://localhost:8000';  // iOS simulator
// const _baseUrl = 'http://192.168.1.10:8000';  // Dispositivo físico

final apiBaseUrlProvider = Provider<String>((ref) => _baseUrl);

final dioProvider = Provider<Dio>((ref) {
  return Dio(BaseOptions(
    baseUrl: ref.watch(apiBaseUrlProvider),
    connectTimeout: const Duration(seconds: 5),
    receiveTimeout: const Duration(seconds: 10),
  ));
});

Persona C importa este provider en sus datasources.


8. Correr la app

En un emulador

# Listar dispositivos disponibles
fvm flutter devices

# Correr en el primer dispositivo
fvm flutter run

# O seleccionar uno específico
fvm flutter run -d <device-id>

En dispositivo físico

  1. Habilita USB Debugging en el teléfono
  2. Conecta el cable
  3. fvm flutter run

9. Verificar que funciona

Módulo D (offline — funciona sin backend)

  1. Navega a /guia (botón en home o URL directa)
  2. Deberías ver 4 categorías: Orgánicos, Reciclables, Sanitarios, Especiales
  3. Prueba el buscador: escribe "pila" → debe mostrar "Especiales"
  4. Toca una categoría → pantalla de detalle con items ✓ y ✗

Módulo C (requiere backend corriendo)

  1. Arranca el backend primero:

    cd basura_backend
    uvicorn app.main:app --reload
    
  2. Arranca el simulador:

    curl -X POST http://localhost:8000/admin/route/RUTA-01/start
    
  3. En Flutter, deberías ver:

    • Ventana horaria: "7:20 pm 7:35 pm"
    • Mensaje: "El camión está en camino..."
    • Badge de estado: "EN_RUTA"
  4. Cada ~10 segundos, el ETA se actualiza automáticamente vía WebSocket


10. Hot Reload y Hot Restart

Hot Reload (r)

Actualiza cambios en la UI sin perder el estado.

# En la terminal donde corre flutter run, presiona:
r

Hot Restart (R)

Reinicia la app desde cero.

R

11. Debugging

Ver logs en consola

fvm flutter run --verbose

Flutter DevTools

fvm flutter pub global activate devtools
fvm flutter pub global run devtools

Abre el link que aparece en el navegador.


12. Build para producción

Android APK

fvm flutter build apk --release

El APK estará en: build/app/outputs/flutter-apk/app-release.apk

iOS (requiere macOS + Xcode)

fvm flutter build ios --release

13. Troubleshooting

"Waiting for another flutter command to release the startup lock"

# Borrar el lock file
rm ~/.dart_tool/dart_tool.lock

"Gradle build failed"

Java incorrecto o problema de versión.

Solución:

# Verificar Java 17
java -version

# Limpiar build
cd android
./gradlew clean
cd ..
fvm flutter clean
fvm flutter pub get

"Could not connect to WebSocket"

Backend no está corriendo o URL incorrecta.

Solución:

# Verifica que el backend esté up
curl http://localhost:8000/health

# Ajusta la URL en api_config.dart según tu entorno

"asset not found: assets/recycling_guide.json"

El asset no está declarado en pubspec.yaml.

Solución:

flutter:
  assets:
    - assets/recycling_guide.json

Luego:

fvm flutter clean
fvm flutter pub get

"Unsupported class file major version 65"

Estás usando Java 21 en lugar de Java 17.

Solución:

# Cambiar a Java 17 (ver setup/00-requisitos.md)
sudo update-alternatives --config java

14. Integración final con Persona A

Cuando Persona A tenga el módulo de Auth listo:

  1. Agregar el JWT token al Dio:

    final dioProvider = Provider<Dio>((ref) {
      final dio = Dio(BaseOptions(baseUrl: _baseUrl));
    
      // Interceptor para agregar token
      dio.interceptors.add(InterceptorsWrapper(
        onRequest: (options, handler) {
          final token = ref.read(authTokenProvider);
          if (token != null) {
            options.headers['Authorization'] = 'Bearer $token';
          }
          handler.next(options);
        },
      ));
    
      return dio;
    });
    
  2. Proteger las rutas:

    GoRoute(
      path: '/',
      redirect: (context, state) {
        final isLoggedIn = ref.read(authProvider).isAuthenticated;
        return isLoggedIn ? null : '/login';
      },
      builder: (_, __) => const ETAHomeScreen(),
    ),
    

Siguiente paso