CULTIVA IA · Revisión Flutter/Dart
Auditoría de Código Flutter/Dart
NutriTrack Pro — App de seguimiento nutricional y fitness
Cliente NutriTrack SL
Fecha 18 Jun 2026
Flutter 3.16 / Dart 3.2
Estado flutter_bloc + GoRouter
Líneas auditadas ~12.000 LOC
Checklist items 87 comprobados
64
/100 puntos
NECESITA MEJORA
5
Criticos
(crashes / seguridad)
12
Altos
(rendimiento / arquitectura)
19
Medios
(calidad / mantenibilidad)
51
Superados
(buenas practicas OK)
!
5 Issues Criticos — Accion Inmediata Requerida
BuildContext usado tras await sin mounted check
Causa crashes en Android cuando el widget se desmonta antes de que termine la llamada a la API. Relacionado con los crashes esporadicos reportados.
nutrition_log_screen.dart:247
API key de Firebase hardcodeada en codigo fuente
La clave de Firebase Web API Key aparece en firebase_options.dart sin ofuscacion ni exclusion de VCS. Riesgo de exposicion critico.
firebase_options.dart:18
Tokens JWT almacenados en SharedPreferences
Los tokens de sesion se guardan en SharedPreferences en texto plano. Usar flutter_secure_storage o equivalente con Keychain/EncryptedSharedPreferences.
auth_repository.dart:89
Stream sin cancelar en dispose() (memory leak)
StreamSubscription de WebSocket de tiempo real no se cancela en dispose(). Produce memory leaks acumulativos que degradan el rendimiento.
realtime_cubit.dart:43
FlutterError.onError no sobreescrito
Los errores de framework (build, layout, paint) no se capturan ni reportan. Sentry/Crashlytics recibe solo una fraccion de los errores reales.
main.dart:12
2
Pitfalls del Lenguaje Dart
45%
✕
Bang operator (!) usado sin comprobacion previa de null
Encontrados 47 usos de ! en produccion. En meal_detail_bloc.dart:156: state.selectedMeal!.nutritionFacts! — si la API retorna null en campos opcionales provoca un crash silencioso.
CRITICO
✕
Futures ignorados (fire-and-forget) sin unawaited()
12 llamadas async sin await ni unawaited(). Los errores se pierden silenciosamente. Principal culpable: analytics_service.dart con 5 casos.
ALTO
✕
var donde final funciona (mutabilidad innecesaria)
~80 variables declaradas con var que nunca se reasignan. Activar prefer_final_locals en analysis_options para detectarlas automaticamente.
MEDIO
!
Pattern matching de Dart 3 no aprovechado
El proyecto usa Dart 3.2 pero mantiene patrones verbosos con is y cast manual. Las sealed classes de estados de BLoC podrían usar switch expressions con exhaustividad garantizada.
MEDIO
✓
Null safety habilitado correctamente (Dart 3 sound null safety)
Todo el proyecto corre en sound null safety. No hay migraciones pendientes de dependencias pre-null-safety.
OK
✓
package: imports usados consistentemente (no relative imports)
OK
✕ Problema detectado — meal_detail_bloc.dart:156
// MAL: crash garantizado si selectedMeal o nutritionFacts es null final calories = state.selectedMeal!.nutritionFacts!.calories; // BIEN: Dart 3 pattern matching con exhaustividad if (state case MealLoaded(:final selectedMeal)) { final calories = selectedMeal.nutritionFacts?.calories ?? 0; }
3
Widget Best Practices
35%
✕
Widgets con build() de 300–600 lineas (megawidgets)
Identificados 6 widgets que superan 200 lineas en build(): DashboardScreen (587L), FoodLogWidget (412L), WeeklyReportCard (342L). Principal causa del tiempo de arranque lento y dificultad en PR reviews.
ALTO
✕
const constructors no usados en widgets estaticos
~130 widgets instanciados sin const cuando todos sus campos son final. Cada rebuild del padre los recrea innecesariamente. Herramienta: flutter analyze --no-fatal-infos | grep prefer_const
ALTO
✕
Llamadas a API dentro de build() via FutureBuilder anidados
En NutritionChartWidget.build() se llama directamente a nutritionRepo.getDailySummary(). Cada rebuild del padre dispara una nueva llamada HTTP. Mover a BLoC/Cubit.
CRITICO
✕
Colores hardcodeados (no usan Theme.of(context).colorScheme)
Encontrados Colors.teal[600], Color(0xFF2E7D32) y otros 23 colores literales. El dark mode esta roto en 4 pantallas por esto.
MEDIO
✓
ValueKey usado en listas dinamicas (preservacion de estado)
La lista de alimentos y el historial de registros usan ValueKey(food.id) correctamente.
OK
✓
SafeArea aplicado en todas las pantallas
OK
4
Gestion de Estado (BLoC/Cubit)
60%
✕
Estados con boolean flags (isLoading + hasError simultaneos posibles)
UserProfileState usa bool isLoading, bool hasError y User? user. El estado isLoading=true && hasError=true es representable e inconsistente. Usar sealed classes.
ALTO
✕
BlocBuilder con scope demasiado amplio (rebuild del arbol entero)
El BlocBuilder<DashboardBloc, DashboardState> en DashboardScreen reconstruye 47 widgets por cada emision de estado (incluidos widgets estaticos). Usar BlocSelector o extraer subtrees con const.
ALTO
!
Estado no implementa == correctamente en 3 clases
FoodSearchState, GoalSettingState y WeeklyReportState no usan Equatable ni freezed. BLoC emite estados identicos y dispara rebuilds innecesarios.
MEDIO
✓
Business logic fuera de widgets — BLoC/Cubit bien separados
En general el equipo mantiene correctamente la logica de negocio en los BLoCs. No hay llamadas directas a repositorios desde widgets (excepto el issue de NutritionChartWidget).
OK
✓
Inyeccion de dependencias via constructor en BLoCs (no ServiceLocator interno)
OK
5
Rendimiento
50%
✕
ListView sin .builder para listas grandes (hasta 300 items)
FoodHistoryScreen usa ListView(children: foods.map(...).toList()) con hasta 300 registros diarios. Construye todos los widgets de golpe, causando el jank en Android gama media.
ALTO
✕
Imagenes de red sin cache (cada scroll recarga)
Image.network() usado directamente en 8 widgets. cached_network_image ya esta en el pubspec.yaml pero no se usa. Sustitucion directa.
ALTO
✕
Opacity widget en animaciones (en vez de AnimatedOpacity)
El skeleton loader de NutritionCard usa Opacity(opacity: animationValue) en un AnimationController. Provoca rasterizacion de toda la capa en cada frame.
MEDIO
✓
Paginacion implementada en busqueda de alimentos
La busqueda de alimentos paginada correctamente (20 items/pagina con scroll infinito). Buen patron a replicar en el historico.
OK
9
Seguridad
25%
✕
Tokens JWT en SharedPreferences (texto plano)
Critico. Un dispositivo con root o backup de Android puede extraer los tokens. Migrar a flutter_secure_storage: usa Android EncryptedSharedPreferences e iOS Keychain sin cambios de API significativos.
CRITICO
✕
API key expuesta en codigo fuente (Firebase)
Ademas del riesgo de abuso, hay datos medicos de usuarios (GDPR). Usar --dart-define en CI/CD y excluir el archivo de VCS. Revocar la clave actual en Firebase Console.
CRITICO
!
Deep links no validados antes de navegar
El handler de GoRouter para deep links no valida el esquema ni los parametros antes de navegar. Vector de ataque para open redirect / injection via links maliciosos.
ALTO
✓
HTTPS forzado en todas las llamadas API (no HTTP permitido)
OK
✓
Input de usuario validado en formularios antes de enviar a API
OK
12
Manejo de Errores
30%
✕
FlutterError.onError no sobreescrito — errores de framework no reportados
Sentry esta integrado pero solo captura errores en el codigo de la app. Los errores de layout/build/paint de Flutter van al stderr sin reportarse. Anadir en main.dart antes de runApp().
CRITICO
✕
BuildContext usado tras await (causa de crashes en produccion)
En NutritionLogScreen._submitLog() se usa Navigator.of(context).pop() y ScaffoldMessenger.of(context).showSnackBar() sin verificar context.mounted despues del await a la API.
CRITICO
!
Excepciones raw de red mostradas al usuario sin mapear
DioException.message se muestra directamente en el SnackBar. El usuario ve mensajes como "SocketException: Connection refused (OS Error: Connection refused, errno = 61)" en vez de "Sin conexion, inténtalo de nuevo".
MEDIO
✓
Sentry integrado con BlocObserver para errores de estado
El SentryBlocObserver captura transiciones y errores de BLoC. Buena base; falta completar con los hooks de FlutterError.
OK
✕ Crash detectado — nutrition_log_screen.dart:247
// MAL: Navigator llamado tras await sin mounted check Future<void> _submitLog() async { await cubit.addLog(_currentEntry); // widget puede desmontarse aqui Navigator.of(context).pop(); // CRASH si widget ya no existe } // BIEN: verificar mounted antes de usar context tras await Future<void> _submitLog() async { await cubit.addLog(_currentEntry); if (!context.mounted) return; // Flutter 3.7+ check Navigator.of(context).pop(); }
★
Plan de Accion por Sprints
Prioridad Accion Impacto Esfuerzo Sprint
P0 Anadir context.mounted checks tras todos los awaits Elimina crashes en produccion 2–3h Inmediato
P0 Migrar tokens a flutter_secure_storage Seguridad critica (GDPR) 4h Inmediato
P0 Revocar clave Firebase y mover a --dart-define Cierra vector de ataque 1h Inmediato
P0 Cancelar StreamSubscription en dispose() en realtime_cubit.dart Elimina memory leak 1h Inmediato
P1 Extraer megawidgets en componentes <100L (DashboardScreen, FoodLogWidget) PR reviews viables, inicio rapido 2 dias Sprint 1
P1 Migrar ListView a ListView.builder en historico Fin del jank en gama media 4h Sprint 1
P1 Sustituir estados boolean por sealed classes en BLoC Elimina estados imposibles 1 dia Sprint 1
P2 Anadir prefer_const_constructors y corregir 130 widgets Mejora startup time ~800ms 3 dias Sprint 2
P2 Activar strict-casts/inference/raw-types en analysis_options Previene futuros type bugs 1 dia + fixes Sprint 2
P2 Usar cached_network_image (ya instalado, no usado) Elimina recargas de imagen 2h Sprint 2