🦋
Revisor de Código Flutter / Dart — CULTIVA IA

Revisión de PR: MealLogPage

NutriTrack Mobile · BLoC + Clean Architecture · PR #47 · Autor: dev-junior@nutritrack.io
Veredicto
BLOQUEADO
Archivos revisados: 3
Tecnología: Dart 3.x flutter_bloc
Arquitectura: Clean Architecture + BLoC
Fecha: 2026-06-18
Archivos revisados
lib/features/meal/presentation/ meal_log_page.dart 2 CRITICAL · 3 HIGH · 2 MEDIUM
lib/features/meal/bloc/ meal_log_bloc.dart 3 CRITICAL · 1 HIGH
lib/data/repositories/ nutrition_repository.dart 1 CRITICAL · 1 HIGH · 2 MEDIUM
Bloquean el merge — riesgo de seguridad o corrupción de datos
🔑 API key hardcodeada en código fuente meal_log_page.dart:56
Problema
La clave de API "sk-nutritrack-prod-abc123xyz" aparece en texto plano en el código fuente. Cualquier persona con acceso al repo (o al APK decomilado) puede extraerla y hacer llamadas autenticadas contra la API de producción.
56  final apiKey = "sk-nutritrack-prod-abc123xyz";  // ⚠️ secreto en texto plano
57  await _repo.logMeal(meal, apiKey);
Corrección
Eliminar el argumento apiKey de la capa de presentación. El repositorio debe leer la clave desde flutter_secure_storage o variables de entorno en build-time (--dart-define). La presentación nunca debe manejar secretos.
Accion inmediata
Revocar la clave expuesta en la consola de NutriTrack API. Rotar credenciales antes de continuar el review.
🌐 Tráfico HTTP sin cifrar (cleartext) nutrition_repository.dart:12, 22
Problema
Todas las llamadas usan http:// en lugar de https://. En Android 9+ el tráfico cleartext está bloqueado por defecto sin android:usesCleartextTraffic="true", lo que causaría fallos en producción. Además expone credenciales y datos de comida en redes no seguras.
12  await http.get(Uri.parse('http://api.nutritrack.io/meals'));
22  await http.post(Uri.parse('http://api.nutritrack.io/log'), ...);
Corrección
Cambiar todas las URLs a https://. Configurar network_security_config.xml para rechazar explícitamente cleartext en Android.
🔄 Estado BLoC mutado in-place (violación de inmutabilidad) meal_log_bloc.dart:17–25
Problema
El handler LoadMeals muta directamente las propiedades del objeto state actual en lugar de emitir un nuevo estado. flutter_bloc compara referencias de objeto; al mutar el mismo objeto, el BlocConsumer nunca detecta cambios y la UI no se actualiza.
17on<LoadMeals>((event, emit) async {
18  state.isLoading = true;           // ❌ mutación directa — el BLoC no emite
19  try {
20    final meals = await repo.fetchMeals();
21    state.meals = meals;              // ❌ mutación directa
22    state.isLoading = false;          // ❌
23  } catch (e) {
24    state.isError = true;             // ❌
25  }
26});
Corrección
Hacer MealLogState inmutable (campos final, añadir copyWith, implementar ==/hashCode con equatable). Usar emit(state.copyWith(isLoading: true)) en cada transición.
// Corrección
on<LoadMeals>((event, emit) async {
  emit(state.copyWith(isLoading: true));
  try {
    final meals = await repo.fetchMeals();
    emit(state.copyWith(meals: meals, isLoading: false, hasData: true));
  } catch (e) {
    emit(state.copyWith(isLoading: false, isError: true));
  }
});
🎯 Boolean flag soup — estados imposibles en BLoC meal_log_bloc.dart:3–11
Problema
El estado modela tres booleanos independientes (isLoading, isError, hasData) que permiten combinaciones imposibles como isLoading=true && isError=true o hasData=true && isLoading=true. Esto genera bugs difíciles de reproducir cuando las transiciones se solapan.
Corrección
Reemplazar los tres booleanos con un tipo sellado que modele estados mutuamente excluyentes:
sealed class MealLogState {}
class MealLogInitial  extends MealLogState {}
class MealLogLoading  extends MealLogState {}
class MealLogSuccess  extends MealLogState {
  const MealLogSuccess(this.meals);
  final List<String> meals;
}
class MealLogFailure  extends MealLogState {
  const MealLogFailure(this.message);
  final String message;
}
🏗️ Repository importa Flutter — violación de capas Clean Architecture nutrition_repository.dart:3
Problema
La capa de datos importa package:flutter/material.dart, introduciendo una dependencia del framework en una capa que debe ser Dart puro. Esto impide reutilizar el repositorio en tests sin levantar el motor Flutter y viola la regla de dependencias de Clean Architecture.
 3import 'package:flutter/material.dart';  // ❌ framework en capa de datos
Corrección
Eliminar el import. Si se necesitan colores o temas para algo, moverlo a la capa de presentación. La capa de datos solo debe importar dart: y paquetes de Dart puros.
🔗 Repositorio instanciado directamente en el BLoC — sin inyección de dependencias meal_log_bloc.dart:13
Problema
MealLogBloc construye NutritionRepository() internamente. Esto hace imposible mockear el repositorio en tests y crea un acoplamiento fuerte entre capas.
13  final NutritionRepository repo = NutritionRepository(); // ❌ instanciación interna
Corrección
Inyectar el repositorio via constructor. Depender de una abstracción (abstract class INutritionRepository) en lugar de la implementación concreta.
MealLogBloc({required INutritionRepository repository})
    : _repository = repository,
      super(MealLogInitial());
Deben corregirse antes del merge — bugs funcionales o degradación de rendimiento
💧 StreamSubscription no se cancela en dispose() — memory leak meal_log_page.dart:18, 43
Problema
_sub se suscribe al stream en initState() pero el widget no implementa dispose(). Al desmontar el widget el listener queda activo, causando leak de memoria y potenciales setState() sobre widget desmontado.
19  StreamSubscription? _sub;  // declarado pero nunca cancelado
24  _sub = _repo.mealUpdates.listen((update) {
25    setState(() { hasData = true; });
26  });
   // ⚠️ sin @override void dispose() { _sub?.cancel(); super.dispose(); }
Corrección
Añadir @override void dispose() { _sub?.cancel(); _searchCtrl.dispose(); super.dispose(); }. Además, TextEditingController también debe disponerse.
🧭 BuildContext usado tras await sin comprobar mounted meal_log_page.dart:60
Problema
Tras el await _repo.logMeal(), el código llama a Navigator.push(context, ...) sin verificar context.mounted. Si el widget se desmontó durante el await, esto lanza una excepción en tiempo de ejecución.
60  await _repo.logMeal(meal, apiKey);
61  Navigator.push(context, ...);  // ❌ context puede estar desmontado
Corrección
Añadir if (!context.mounted) return; inmediatamente después del await (Flutter 3.7+).
🔢 Ordenación costosa en build() — trabajo en capa de presentación meal_log_page.dart:37–40, 69
Problema
_getSortedMeals() ordena la lista en cada llamada a build(). Con listas grandes esto degrada el rendimiento de 60fps. La ordenación debe hacerse en el BLoC al emitir el estado.
69  final meals = _getSortedMeals(state.meals);  // ❌ O(n log n) en cada rebuild
Corrección
Ordenar en el BLoC antes de emitir (meals.sort() en el handler) y exponer la lista ya ordenada en el estado. El widget solo consume.
🪟 BlocConsumer envuelve Scaffold completo — rebuild innecesario de toda la pantalla meal_log_page.dart:66
Problema
El BlocConsumer envuelve el Scaffold entero. Cualquier cambio de estado (loading, error) reconstruye toda la pantalla incluyendo el AppBar, el TextField y el botón, que no dependen del estado.
Corrección
Acotar el consumer solo al subtree que varía (el ListView y el indicador de carga). Usar BlocSelector para suscribirse solo a los campos relevantes.
🗒️ Logging de errores con print() en producción meal_log_bloc.dart:24
Problema
print("Error loading meals: $e") escribe en la consola en builds de producción. Puede filtrar stack traces con información sensible. Además el error queda silenciado — no se emite estado de error al BLoC.
24  print("Error loading meals: $e");  // ❌ print en producción + error silenciado
Corrección
Reemplazar print() por dart:developer log() o el logger del proyecto. Integrar Sentry/Crashlytics para captura no fatal. El estado de error debe emitirse para que la UI lo gestione.
Mejoras de mantenibilidad y accesibilidad — recomendadas antes del merge
🎨 Colores y estilos hardcodeados — rompe dark mode meal_log_page.dart:47–48
Problema
Color(0xFF4CAF50) y TextStyle(color: Colors.white, fontSize: 16) están hardcodeados. Si la app soporta dark mode o el usuario cambia el contraste del sistema, estos elementos quedarán ilegibles.
Corrección
Usar Theme.of(context).colorScheme.primary y Theme.of(context).textTheme.bodyMedium. Definir tokens de diseño en el tema global de la app.
🧩 Helper method _buildMealItem() — impide optimizaciones del framework meal_log_page.dart:42–51
Problema
Los métodos privados _buildMealItem() que retornan widgets impiden que Flutter optimize las reconstrucciones parciales. El framework no puede cachear ni comparar estos subtrees porque son funciones, no clases.
Corrección
Extraer a una clase StatelessWidget con constructor const:
class MealListItem extends StatelessWidget {
  const MealListItem({super.key, required this.meal});
  final String meal;
  @override
  Widget build(BuildContext context) { /* ... */ }
}
🪢 BuildContext almacenado en campo de instancia meal_log_page.dart:30, 68
Problema
_ctx = context almacena el contexto en un campo del State. Si el widget se reasigna a otro árbol de elementos, _ctx apunta a un contexto obsoleto, lo que puede causar crashes o comportamientos inesperados.
Corrección
Eliminar el campo _ctx. Usar siempre el parámetro context del método build() o del callback del BlocConsumer.
ElevatedButton sin semanticLabel — accesibilidad insuficiente meal_log_page.dart:77–79
Problema
El botón "Registrar" no tiene un Semantics wrapping descriptivo. Los lectores de pantalla (TalkBack/VoiceOver) leerán solo "Registrar" sin contexto de la acción. El icono de estado de carga (CircularProgressIndicator) tampoco tiene etiqueta semántica.
Corrección
Envolver el indicador en Semantics(label: 'Cargando comidas', child: ...). Añadir tooltip o Semantics(button: true, label: 'Registrar comida seleccionada') al botón.
Deuda técnica — planificar en próximos sprints
📋 Sin analysis_options.yaml con reglas estrictas analysis_options.yaml
Problema
El proyecto no tiene analysis_options.yaml configurado con strict-casts, strict-inference, ni strict-raw-types. Esto permite que muchos de los problemas detectados pasen el análisis estático sin warnings.
Corrección
Añadir reglas de análisis estrictas. Comenzar con el paquete flutter_lints y activar progresivamente las reglas de very_good_analysis o pedantic_mono.
# analysis_options.yaml
include: package:flutter_lints/flutter.yaml
analyzer:
  language:
    strict-casts: true
    strict-inference: true
    strict-raw-types: true
📊

Resumen de la Revision

Severidad Cantidad Estado
CRITICAL 6 ⛔ BLOQUEA MERGE
HIGH 5 ⛔ BLOQUEA MERGE
MEDIUM 4 ⚠️ RECOMENDADO
LOW 1 ℹ️ DEUDA TECNICA
Veredicto: BLOQUEADO
6 issues CRITICAL (incluyendo secreto expuesto y tráfico HTTP) + 5 HIGH deben corregirse antes del merge. Prioridad inmediata: revocar la API key hardcodeada.