Mejores Prácticas para Revisión de Código Flutter/Dart
Lista de verificación integral e independiente de librerías para revisar aplicaciones Flutter/Dart. Estos principios se aplican independientemente de la solución de gestión de estado, librería de enrutamiento o marco de inyección de dependencias utilizado.
1. Salud General del Proyecto
- El proyecto sigue una estructura de carpetas consistente (primero por funcionalidad o por capas)
- Separación adecuada de preocupaciones: capas de UI, lógica de negocio y datos
- No hay lógica de negocio en los widgets; los widgets son puramente de presentación
-
pubspec.yamlestá limpio — sin dependencias no utilizadas, versiones fijadas adecuadamente -
analysis_options.yamlincluye un conjunto de reglas estrictas con la configuración estricta del analizador habilitada - No hay sentencias
print()en el código de producción — usadart:developerlog()o un paquete de registro - Los archivos generados (
.g.dart,.freezed.dart,.gr.dart) están actualizados o en.gitignore - El código específico de la plataforma está aislado detrás de abstracciones
2. Errores Comunes del Lenguaje Dart
-
dynamicimplícito: Faltan anotaciones de tipo que llevan adynamic— habilitastrict-casts,strict-inference,strict-raw-types - Mal uso de la seguridad de nulos: Uso excesivo de
!(operador bang) en lugar de comprobaciones de nulo adecuadas o coincidencia de patrones de Dart 3 (if (value case var v?)) - Fallos en la promoción de tipos: Usar
this.fielddonde funcionaría la promoción de variables locales - Capturar de forma demasiado amplia:
catch (e)sin cláusulaon; siempre especifica los tipos de excepción - Capturar
Error: Los subtipos deErrorindican errores y no deben capturarse -
asyncno utilizado: Funciones marcadas comoasyncque nunca usanawait— sobrecarga innecesaria - Uso excesivo de
late:lateusado donde una variable nullable o la inicialización en el constructor serían más seguras; difiere errores al tiempo de ejecución - Concatenación de cadenas en bucles: Usa
StringBufferen lugar de+para la construcción iterativa de cadenas - Estado mutable en contextos
const: Los campos en clases de constructorconstno deben ser mutables - Ignorar valores de retorno de
Future: Usaawaito llama explícitamente aunawaited()para indicar la intención -
varcuandofinalfunciona: Prefierefinalpara variables locales yconstpara constantes en tiempo de compilación - Importaciones relativas: Usa importaciones
package:para consistencia - Colecciones mutables expuestas: Las API públicas deben devolver vistas no modificables, no
List/Mapcrudos - Falta de coincidencia de patrones de Dart 3: Prefiere expresiones
switchyif-casesobre comprobacionesisverbosas y casting manual - Clases desechables para múltiples retornos: Usa registros de Dart 3
(String, int)en lugar de DTOs de un solo uso -
print()en código de producción: Usadart:developerlog()o el paquete de registro del proyecto;print()no tiene niveles de registro y no se puede filtrar
3. Mejores Prácticas de Widgets
Descomposición de widgets:
- Ningún widget con un método
build()que supere ~80-100 líneas - Widgets divididos por encapsulación Y por cómo cambian (límites de reconstrucción)
- Los métodos auxiliares privados
_build*()que devuelven widgets se extraen a clases de widget separadas (permite la reutilización de elementos, la propagación deconsty las optimizaciones del framework) - Se prefieren widgets sin estado (
StatelessWidget) sobre los que tienen estado (StatefulWidget) cuando no se necesita estado mutable local - Los widgets extraídos están en archivos separados cuando son reutilizables
Uso de const:
- Se usan constructores
constsiempre que sea posible — evita reconstrucciones innecesarias - Literales
constpara colecciones que no cambian (const [],const {}) - El constructor se declara
constcuando todos los campos sonfinal
Uso de claves (Key):
- Se usa
ValueKeyen listas/grids para preservar el estado a través de reordenaciones -
GlobalKeyse usa con moderación — solo cuando realmente se necesita acceder al estado a través del árbol - Se evita
UniqueKeyenbuild()— fuerza la reconstrucción en cada fotograma - Se usa
ObjectKeycuando la identidad se basa en un objeto de datos en lugar de un valor único
Tematización y sistema de diseño:
- Los colores provienen de
Theme.of(context).colorScheme— sin coloresColors.redo valores hexadecimales hardcodeados - Los estilos de texto provienen de
Theme.of(context).textTheme— sinTextStyleen línea con tamaños de fuente crudos - Compatibilidad con modo oscuro verificada — sin asumir fondo claro
- El espaciado y el tamaño usan tokens de diseño consistentes o constantes, no números mágicos
Complejidad del método build():
- Sin llamadas de red, E/S de archivos ni cálculos pesados en
build() - Sin
Future.then()ni trabajoasyncenbuild() - Sin creación de suscripciones (
.listen()) enbuild() -
setState()localizado en el subárbol más pequeño posible
4. Gestión de Estado (Independiente de Librería)
Estos principios se aplican a todas las soluciones de gestión de estado de Flutter (BLoC, Riverpod, Provider, GetX, MobX, Signals, ValueNotifier, etc.).
Arquitectura:
- La lógica de negocio vive fuera de la capa de widgets — en un componente de gestión de estado (BLoC, Notifier, Controller, Store, ViewModel, etc.)
- Los gestores de estado reciben dependencias a través de inyección, no construyéndolas internamente
- Una capa de servicio o repositorio abstrae las fuentes de datos — los widgets y gestores de estado no deben llamar a APIs o bases de datos directamente
- Los gestores de estado tienen una única responsabilidad — sin gestores "dios" que manejen preocupaciones no relacionadas
- Las dependencias entre componentes siguen las convenciones de la solución:
- En Riverpod: se espera que los proveedores dependan de otros proveedores mediante
ref.watch— solo señalar cadenas circulares o demasiado enredadas - En BLoC: los blocs no deben depender directamente de otros blocs — prefiere repositorios compartidos o coordinación en la capa de presentación
- En otras soluciones: sigue las convenciones documentadas para la comunicación entre componentes
- En Riverpod: se espera que los proveedores dependan de otros proveedores mediante
Inmutabilidad e igualdad de valores (para soluciones de estado inmutable: BLoC, Riverpod, Redux):
- Los objetos de estado son inmutables — se crean nuevas instancias mediante
copyWith()o constructores, nunca se mutan en el lugar - Las clases de estado implementan
==yhashCodecorrectamente (todos los campos incluidos en la comparación) - El mecanismo es consistente en todo el proyecto — anulación manual,
Equatable,freezed, registros de Dart u otros - Las colecciones dentro de los objetos de estado no se exponen como
List/Mapmutables crudos
Disciplina de reactividad (para soluciones de mutación reactiva: MobX, GetX, Signals):
- El estado solo se muta a través de la API reactiva de la solución (
@actionen MobX,.valueen signals,.obsen GetX) — la mutación directa de campos evita el seguimiento de cambios - Los valores derivados usan el mecanismo de computación de la solución en lugar de almacenarse de forma redundante
- Las reacciones y los dispositivos de limpieza se limpian adecuadamente (
ReactionDisposeren MobX, limpieza de efectos en Signals)
Diseño de la forma del estado:
- Los estados mutuamente excluyentes usan tipos sellados, variantes de unión o el tipo de estado asíncrono incorporado de la solución (por ejemplo,
AsyncValuede Riverpod) — no banderas booleanas (isLoading,isError,hasData) - Cada operación asíncrona modela los estados de carga, éxito y error como estados distintos
- Todas las variantes de estado se manejan exhaustivamente en la UI — sin casos ignorados silenciosamente
- Los estados de error llevan información de error para mostrar; los estados de carga no llevan datos obsoletos
- Los datos anulables no se utilizan como indicador de carga — los estados son explícitos
// MALO — la mezcla de banderas booleanas permite estados imposibles
class UserState {
bool isLoading = false;
bool hasError = false; // ¡isLoading && hasError es representable!
User? user;
}
// BUENO (enfoque inmutable) — los tipos sellados hacen que los estados imposibles no sean representables
sealed class UserState {}
class UserInitial extends UserState {}
class UserLoading extends UserState {}
class UserLoaded extends UserState {
final User user;
const UserLoaded(this.user);
}
class UserError extends UserState {
final String message;
const UserError(this.message);
}
// BUENO (enfoque reactivo) — observable enum + datos, mutaciones a través de la API de reactividad
// enum UserStatus { initial, loading, loaded, error }
// Usa el observable/signal de tu solución para envolver el estado y los datos por separado
Optimización de reconstrucción:
- Los widgets consumidores de estado (Builder, Consumer, Observer, Obx, Watch, etc.) se limitan al ámbito más estrecho posible
- Se usan selectores para reconstruir solo cuando cambian campos específicos — no en cada emisión de estado
- Se usan widgets
constpara detener la propagación de la reconstrucción a través del árbol - El estado computado/derivado se calcula de forma reactiva, no se almacena de forma redundante
Suscripciones y disposición:
- Todas las suscripciones manuales (
.listen()) se cancelan endispose()/close() - Los controladores de flujo (
StreamController) se cierran cuando ya no son necesarios - Los temporizadores se cancelan en el ciclo de vida de disposición
- Se prefiere el ciclo de vida gestionado por el framework sobre la suscripción manual (constructores declarativos sobre
.listen()) - Verificación de
mountedantes desetStateen callbacks asíncronos -
BuildContextno se usa después deawaitsin verificarcontext.mounted(Flutter 3.7+) — un contexto obsoleto causa fallos - Sin navegación, diálogos o mensajes de scaffold después de brechas asíncronas sin verificar que el widget sigue montado
-
BuildContextnunca se almacena en singletons, gestores de estado o campos estáticos
Estado local vs global:
- El estado efímero de la UI (checkbox, slider, animación) usa estado local (
setState,ValueNotifier) - El estado compartido se eleva solo hasta donde sea necesario — no se globaliza en exceso
- El estado específico de una funcionalidad se elimina adecuadamente cuando la funcionalidad ya no está activa
5. Rendimiento
Reconstrucciones innecesarias:
-
setState()no se llama al nivel del widget raíz — localiza los cambios de estado - Se usan widgets
constpara detener la propagación de la reconstrucción - Se usa
RepaintBoundaryalrededor de subárboles complejos que se repintan de forma independiente - Se usa el parámetro
childdeAnimatedBuilderpara subárboles independientes de la animación
Operaciones costosas en build():
- Sin ordenar, filtrar ni mapear grandes colecciones en
build()— calcular en la capa de gestión de estado - Sin compilación de expresiones regulares en
build() - El uso de
MediaQuery.of(context)es específico (por ejemplo,MediaQuery.sizeOf(context))
Optimización de imágenes:
- Las imágenes de red usan caché (cualquier solución de caché adecuada para el proyecto)
- Resolución de imagen apropiada para el dispositivo de destino (no cargar imágenes 4K para miniaturas)
-
Image.assetconcacheWidth/cacheHeightpara decodificar al tamaño de visualización - Widgets de marcador de posición y error proporcionados para imágenes de red
Carga diferida:
- Se usa
ListView.builder/GridView.builderen lugar deListView(children: [...])para listas grandes o dinámicas (los constructores concretos están bien para listas pequeñas y estáticas) - Paginación implementada para grandes conjuntos de datos
- Carga diferida (
deferred as) utilizada para librerías pesadas en compilaciones web
Otros:
- Se evita el widget
Opacityen animaciones — usaAnimatedOpacityoFadeTransition - Se evita el recorte en animaciones — recortar imágenes previamente
-
operator ==no se anula en widgets — usa constructoresconsten su lugar - Los widgets de dimensiones intrínsecas (
IntrinsicHeight,IntrinsicWidth) se usan con moderación (paso de diseño adicional)
6. Pruebas
Tipos y expectativas de pruebas:
- Pruebas unitarias: Cubren toda la lógica de negocio (gestores de estado, repositorios, funciones de utilidad)
- Pruebas de widgets: Cubren el comportamiento individual del widget, las interacciones y la salida visual
- Pruebas de integración: Cubren flujos de usuario críticos de principio a fin
- Pruebas golden: Comparaciones perfectas de píxeles para componentes de UI críticos de diseño
Objetivos de cobertura:
- Apuntar a un 80%+ de cobertura de línea en la lógica de negocio
- Todas las transiciones de estado tienen pruebas correspondientes (carga → éxito, carga → error, reintento, etc.)
- Casos límite probados: estados vacíos, estados de error, estados de carga, valores límite
Aislamiento de pruebas:
- Las dependencias externas (clientes API, bases de datos, servicios) se simulan o falsean
- Cada archivo de prueba prueba exactamente una clase/unidad
- Las pruebas verifican el comportamiento, no los detalles de implementación
- Los stubs definen solo el comportamiento necesario para cada prueba (stubbing mínimo)
- No hay estado mutable compartido entre casos de prueba
Calidad de las pruebas de widgets:
-
pumpWidgetypumpse usan correctamente para operaciones asíncronas -
find.byType,find.text,find.byKeyse usan adecuadamente - No hay pruebas inestables que dependan del tiempo — usa
pumpAndSettleopump(Duration)explícito - Las pruebas se ejecutan en CI y los fallos bloquean las fusiones
7. Accesibilidad
Widgets semánticos:
- Se usa el widget
Semanticspara proporcionar etiquetas de lector de pantalla donde las etiquetas automáticas son insuficientes - Se usa
ExcludeSemanticspara elementos puramente decorativos - Se usa
MergeSemanticspara combinar widgets relacionados en un único elemento accesible - Las imágenes tienen la propiedad
semanticLabelestablecida
Soporte de lector de pantalla:
- Todos los elementos interactivos son enfocables y tienen descripciones significativas
- El orden de enfoque es lógico (sigue el orden de lectura visual)
Accesibilidad visual:
- Relación de contraste >= 4.5:1 para texto sobre fondo
- Los objetivos táctiles tienen al menos 48x48 píxeles
- El color no es el único indicador de estado (usar iconos/texto junto con él)
- El texto escala con la configuración del tamaño de fuente del sistema
Accesibilidad de interacción:
- Sin callbacks
onPressedque no hagan nada — cada botón hace algo o está deshabilitado - Los campos con error sugieren correcciones
- El contexto no cambia inesperadamente mientras el usuario ingresa datos
8. Preocupaciones Específicas de la Plataforma
Diferencias iOS/Android:
- Se usan widgets adaptativos a la plataforma cuando corresponde
- La navegación hacia atrás se maneja correctamente (botón de retroceso de Android, deslizar hacia atrás en iOS)
- La barra de estado y el área segura se manejan mediante el widget
SafeArea - Los permisos específicos de la plataforma se declaran en
AndroidManifest.xmleInfo.plist
Diseño responsivo:
- Se usa
LayoutBuilderoMediaQuerypara diseños responsivos - Los puntos de interrupción se definen de forma consistente (teléfono, tableta, escritorio)
- El texto no se desborda en pantallas pequeñas — usa
Flexible,Expanded,FittedBox - La orientación horizontal se prueba o se bloquea explícitamente
- Específico para web: se admiten interacciones de mouse/teclado, estados hover presentes
9. Seguridad
Almacenamiento seguro:
- Datos sensibles (tokens, contraseñas) almacenados en almacenamiento seguro de la plataforma (
flutter_secure_storageo equivalente) - No almacenar datos sensibles en texto plano en preferencias compartidas o almacenamiento local
- Usar HTTPS para todas las comunicaciones de red — sin tráfico HTTP
- Validar y sanitizar todas las entradas del usuario para prevenir inyección de código
- Implementar manejo adecuado de sesiones y tokens JWT
- Verificar que las URLs de red sean seguras y no apunten a destinos maliciosos
Prevención de ataques comunes:
- Implementar limitación de velocidad en formularios de inicio de sesión
- Usar CSRF tokens para operaciones sensibles
- No exponer información interna del sistema en mensajes de error
- Implementar cifrado de datos en reposo cuando sea necesario