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