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.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.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.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.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.
package: imports usados consistentemente (no relative imports)
✕ 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.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_constLlamadas 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.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.ValueKey usado en listas dinamicas (preservacion de estado)
La lista de alimentos y el historial de registros usan
ValueKey(food.id) correctamente.SafeArea aplicado en todas las pantallas
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.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.Estado no implementa == correctamente en 3 clases
FoodSearchState, GoalSettingState y WeeklyReportState no usan Equatable ni freezed. BLoC emite estados identicos y dispara rebuilds innecesarios.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).
Inyeccion de dependencias via constructor en BLoCs (no ServiceLocator interno)
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.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.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.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.
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.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.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.
HTTPS forzado en todas las llamadas API (no HTTP permitido)
Input de usuario validado en formularios antes de enviar a API
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().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.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".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.✕ 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 |