Mejores Prácticas de Revisión de Código Flutter/Dart
Lista de verificación completa 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 DI que se utilice.
1. Salud General del Proyecto
- El proyecto sigue una estructura de carpetas consistente (primero características o primero capas)
- Separación adecuada de preocupaciones: capas de UI, lógica de negocio y datos
- Sin lógica de negocio en widgets; los widgets son puramente de presentación
-
pubspec.yamlestá limpio — sin dependencias no utilizadas, versiones fijadas adecuadamente -
analysis_options.yamlincluye un conjunto de lint estricto con configuraciones estrictas del analizador habilitadas - Sin sentencias
print()en código de producción — usadart:developerlog()o un paquete de logging - 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 del Lenguaje Dart
- Dinámico implícito: Falta de anotaciones de tipo que lleva a
dynamic— habilitastrict-casts,strict-inference,strict-raw-types - Mal uso de seguridad nula: Uso excesivo de
!(operador bang) en lugar de comprobaciones nulas adecuadas o coincidencia de patrones de Dart 3 (if (value case var v?)) - Fallos de promoción de tipo: Usar
this.fielddonde funcionaría la promoción de variables locales - Captura demasiado amplia:
catch (e)sin cláusulaon; siempre especifica los tipos de excepción - Capturar
Error: Los subtipos deErrorindican errores y no deben ser capturados -
asyncno utilizado: Funciones marcadas comoasyncque nunca hacenawait— sobrecarga innecesaria - Uso excesivo de
late: Se usalatedonde sería más seguro usar nullable o inicialización en constructor; 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 con constructoresconstno deben ser mutables - Ignorar valores de retorno de
Future: Usaawaito llama explícitamente aunawaited()para indicar la intención -
vardonde funcionafinal: Prefierefinalpara variables locales yconstpara constantes en tiempo de compilación - Importaciones relativas: Usa importaciones
package:para consistencia - Colecciones mutables expuestas: Las APIs públicas deben devolver vistas no modificables, no
List/Mapsin procesar - Falta de coincidencia de patrones de Dart 3: Prefiere expresiones switch y
if-caseen lugar de comprobacionesisverbosas y casting manual - Clases desechables para retornos múltiples: 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 logging del proyecto;print()no tiene niveles de log y no se puede filtrar
3. Mejores Prácticas de Widgets
Descomposición de widgets:
- Ningún widget con un método
build()que exceda ~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 de const y las optimizaciones del marco) - Widgets Stateless preferidos sobre Stateful cuando no se necesita estado mutable local
- Los widgets extraídos están en archivos separados cuando son reutilizables
Uso de Const:
- Constructores
constutilizados siempre que sea posible — previene reconstrucciones innecesarias - Literales
constpara colecciones que no cambian (const [],const {}) - El constructor se declara
constcuando todos los campos son final
Uso de Key:
-
ValueKeyutilizado en listas/grids para preservar el estado a través de reordenamientos -
GlobalKeyutilizado con moderación — solo cuando realmente se necesita acceder al estado a través del árbol -
UniqueKeyevitado enbuild()— fuerza la reconstrucción en cada fotograma -
ObjectKeyutilizado cuando la identidad se basa en un objeto de datos en lugar de un solo valor
Tematización y sistema de diseño:
- Los colores provienen de
Theme.of(context).colorScheme— sinColors.redo valores hexadecimales hardcodeados - Los estilos de texto provienen de
Theme.of(context).textTheme— sinTextStyleen línea con tamaños de fuente sin procesar - Compatibilidad con modo oscuro verificada — sin suposiciones sobre fondo claro
- El espaciado y el tamaño utilizan 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 o cálculos pesados en
build() - Sin
Future.then()o 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ías)
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 reside 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 mediante 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— marca solo cadenas circulares o demasiado enredadas - En BLoC: los blocs no deben depender directamente de otros blocs — prefiere repositorios compartidos o coordinación de 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 valor (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 otro - Las colecciones dentro de los objetos de estado no se exponen como
List/Mapmutables sin procesar
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 utilizan el mecanismo computado de la solución en lugar de almacenarse de forma redundante
- Las reacciones y los removedores se limpian adecuadamente (
ReactionDisposeren MobX, limpieza de efectos en Signals)
Diseño de forma del estado:
- Los estados mutuamente excluyentes utilizan tipos sellados, variantes de unión o el tipo de estado asíncrono integrado de la solución (ej.
AsyncValuede Riverpod) — no indicadores booleanos (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 de forma exhaustiva 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
// MAL — la sopa de indicadores booleanos permite estados imposibles
class UserState {
bool isLoading = false;
bool hasError = false; // ¡isLoading && hasError es representable!
User? user;
}
// BIEN (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);
}
// BIEN (enfoque reactivo) — enumeración observable + datos, mutaciones a través de API de reactividad
// enum UserStatus { initial, loading, loaded, error }
// Usa el observable/signal de tu solución para envolver estado y datos por separado
Optimización de reconstrucción:
- Los widgets consumidores de estado (Builder, Consumer, Observer, Obx, Watch, etc.) se limitan al alcance más estrecho posible
- Se utilizan selectores para reconstruir solo cuando cambian campos específicos — no en cada emisión de estado
- Se utilizan widgets
constpara detener la propagación de reconstrucción a través del árbol - El estado computado/derivado se calcula de forma reactiva, no se almacena de forma redundante
Suscripciones y eliminación:
- Todas las suscripciones manuales (
.listen()) se cancelan endispose()/close() - Los controladores de flujo se cierran cuando ya no son necesarios
- Los temporizadores se cancelan en el ciclo de vida de eliminación
- Se prefiere el ciclo de vida administrado por el marco sobre la suscripción manual (constructores declarativos sobre
.listen()) - Verificación de
mountedantes desetStateen callbacks asíncronos - No se usa
BuildContextdespués deawaitsin verificarcontext.mounted(Flutter 3.7+) — el contexto obsoleto causa fallos - Sin navegación, diálogos o mensajes de scaffold después de brechas asíncronas sin verificar que el widget todavía esté montado
-
BuildContextnunca se almacena en singletons, gestores de estado o campos estáticos
Estado local vs global:
- El estado de UI efímero (checkbox, slider, animación) utiliza estado local (
setState,ValueNotifier) - El estado compartido se eleva solo tan alto como sea necesario — no se globaliza en exceso
- El estado específico de una característica se elimina adecuadamente cuando la característica ya no está activa
5. Rendimiento
Reconstrucciones innecesarias:
- No se llama a
setState()al nivel del widget raíz — localiza los cambios de estado - Se utilizan widgets
constpara detener la propagación de reconstrucción - Se utiliza
RepaintBoundaryalrededor de subárboles complejos que se repintan independientemente - El parámetro
childdeAnimatedBuilderse utiliza para subárboles independientes de la animación
Operaciones costosas en build():
- Sin ordenamiento, filtrado o mapeo de colecciones grandes en
build()— calcula en la capa de gestión de estado - Sin compilación de regex en
build() - El uso de
MediaQuery.of(context)es específico (ej.,MediaQuery.sizeOf(context))
Optimización de imágenes:
- Las imágenes de red utilizan caché (cualquier solución de caché apropiada para el proyecto)
- Resolución de imagen apropiada para el dispositivo de destino (sin cargar imágenes 4K para miniaturas)
-
Image.assetconcacheWidth/cacheHeightpara decodificar al tamaño de visualización - Se proporcionan widgets de marcador de posición y error para imágenes de red
Carga diferida:
- Se utiliza
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) - Se implementa la paginación para conjuntos de datos grandes
- Se utiliza la carga diferida (
deferred as) para librerías pesadas en compilaciones web
Otros:
- Se evita el widget
Opacityen animaciones — utilizaAnimatedOpacityoFadeTransition - Se evita el recorte en animaciones — recorta las imágenes de antemano
- No se anula
operator ==en widgets — utiliza constructoresconsten su lugar - Los widgets de dimensiones intrínsecas (
IntrinsicHeight,IntrinsicWidth) se utilizan con moderación (pase de diseño adicional)
6. Pruebas
Tipos de prueba y expectativas:
- Pruebas unitarias: Cubren toda la lógica de negocio (gestores de estado, repositorios, funciones de utilidad)
- Pruebas de widgets: Cubren el comportamiento individual de los widgets, las interacciones y la salida visual
- Pruebas de integración: Cubren flujos de usuario críticos de principio a fin
- Pruebas golden: Comparaciones de píxeles perfectos para componentes de UI críticos para el diseño
Objetivos de cobertura:
- Apunta a una cobertura de línea del 80%+ 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 de API, bases de datos, servicios) se simulan o falsifican
- 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)
- Sin estado mutable compartido entre casos de prueba
Calidad de las pruebas de widgets:
-
pumpWidgetypumpse utilizan correctamente para operaciones asíncronas -
find.byType,find.text,find.byKeyse utilizan adecuadamente - Sin pruebas inestables que dependan del tiempo — utiliza
pumpAndSettleopump(Duration)explícito - Las pruebas se ejecutan en CI y los fallos bloquean las fusiones
7. Accesibilidad
Widgets semánticos:
- El widget
Semanticsse utiliza para proporcionar etiquetas de lector de pantalla donde las etiquetas automáticas son insuficientes -
ExcludeSemanticsse utiliza para elementos puramente decorativos -
MergeSemanticsse utiliza para combinar widgets relacionados en un único elemento accesible - Las imágenes tienen la propiedad
semanticLabelconfigurada
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 contra fondo
- Los objetivos táctiles tienen al menos 48x48 píxeles
- El color no es el único indicador de estado (utiliza iconos/texto junto a él)
- El texto escala con la configuración del tamaño de fuente del sistema
Accesibilidad de interacción:
- Sin callbacks
onPressedsin operación — cada botón hace algo o está deshabilitado - Los campos de error sugieren correcciones
- El contexto no cambia inesperadamente mientras el usuario está ingresando datos
8. Preocupaciones Específicas de la Plataforma
Diferencias iOS/Android:
- Se utilizan widgets adaptativos de 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 utiliza
LayoutBuilderoMediaQuerypara diseños responsivos - Los puntos de interrupción se definen de manera consistente (teléfono, tableta, escritorio)
- El texto no se desborda en pantallas pequeñas — utiliza
Flexible,Expanded,FittedBox - La orientación horizontal se prueba o se bloquea explícitamente
- Específico de web: se admiten interacciones de mouse/teclado, hay estados de hover presentes
9. Seguridad
Almacenamiento seguro:
- Los datos sensibles