Encapsulando validaciones en Flutter: un enfoque limpio

¿Cómo lograr que tus widgets de Flutter se validen solos? Con typedef y Function encapsulas la lógica de validación dentro del propio componente, manteniendo un código más limpio, reutilizable y desacoplado.

Cuando desarrollamos formularios en Flutter solemos encontrarnos con un problema recurrente: ¿dónde colocar la lógica de validación?

Muchas veces terminamos centralizando las validaciones en el Form o dispersándolas entre controladores y widgets, lo que hace que el código crezca desordenado y difícil de mantener. Para solucionar esto, podemos aplicar un enfoque diferente: hacer que cada widget encapsule su propia validación, pero al mismo tiempo exponerla hacia el padre para que el formulario completo pueda controlarla. En este artículo te muestro cómo lo resolví usando typedef y Function

Ejemplo real:

Imaginemos que tenemos un dropdown o widgets personalizados que queremos validar cuando el usuario presione un botón como en la siguiente imagen: Alt

Resultado esperado

Alt

Normalmente para lograr este comportamiento expondríamos una función de validación y haríamos la lógica de la validación en el componente padre que tiene el botón sin embargo esto rompería el Principio de responsabilidad única . Por qué? Pues el padre pasaría a tener dos responsabilidades:

  1. Coordinar los widgets del formulario.
  2. Conocer y ejecutar la lógica interna de validación de cada uno.

Eso genera acoplamiento y hace que el código sea menos mantenible: si mañana cambias la forma en que se valida el dropdown, también tendrías que tocar el padre.

Como lo solucionamos?

La idea es que cada widget encapsule su validación, pero al mismo tiempo pueda exponer un método que el padre pueda invocar sin necesidad de saber cómo funciona internamente.

Para lograrlo, definimos un builder que recibe el contexto y un método de validación desde el hijo:

Loading syntax highlighting...
// Writing custom builder to execute method from parent widget import 'package:flutter/material.dart'; typedef ValidateBuilder = void Function( BuildContext context, bool Function() methodFromChild, );

En el widget personalizado lo declaramos de esta forma:

Loading syntax highlighting...
class ACPDropdown extends StatefulWidget { final ValidateBuilder? builder; ...resto const ACPDropdown({ super.key, this.builder, ...resto });

También creamos la función que validará nuestros datos y actualizará nuestro estado

Loading syntax highlighting...
bool validate() { if (_selectedItem?.isEmpty ?? true) { setState(() { validation = context.translate('required_field'); }); if (validation.isNotEmpty) { return false; } else { return true; } } return true; }

Y en nuestro estado inicial

Loading syntax highlighting...
@override void initState() { super.initState(); if (widget.builder != null) { widget.builder!.call(context, validate); // <<<<< call it here } }

Ahora solo nos queda invocarlo desde el padre

Loading syntax highlighting...
// Widget padre para ejecutar la validación class MyForm extends StatefulWidget { const MyForm({super.key}); @override State<MyForm> createState() => _MyFormState(); } class _MyFormState extends State<MyForm> { late bool Function() validationCategory; @override Widget build(BuildContext context) { return Padding( padding: const EdgeInsets.all(20.0), child: Column( children: [ ACPDropdown( title: 'Categoría', items: [ {'value': 'tech', 'text': 'Tecnología'}, {'value': 'food', 'text': 'Comida'}, ], builder: (context, methodFromChild) { validationCategory = methodFromChild; }, ), const SizedBox(height: 20), ElevatedButton( onPressed: () { if (validationCategory()) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Formulario válido ✅')), ); } else { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Revisa los campos ❌')), ); } }, child: const Text('Guardar'), ), ], ), ); } }

Conclusión

Con este enfoque conseguimos varios objetivos importantes:

  1. Encapsulación de la lógica de validación: Cada widget es responsable de validar su propio estado, sin que el formulario padre tenga que conocer los detalles internos.
  2. Código más limpio y mantenible: Al evitar que el padre gestione la lógica de todos los widgets, reducimos el acoplamiento y hacemos que cualquier cambio en la validación se limite al propio widget.
  3. Reutilización y flexibilidad: Gracias al ValidateBuilder, podemos exponer el método de validación al padre de forma controlada, permitiendo dispararlo cuando sea necesario, por ejemplo al presionar un botón.
  4. Cumplimiento de principios SOLID: Especialmente el Principio de Responsabilidad Única y la Inversión de Dependencias, manteniendo un diseño escalable y robusto.

En resumen, este patrón nos permite crear formularios inteligentes y modulares en Flutter, donde cada componente es dueño de su comportamiento pero sigue cooperando con el flujo global del formulario. Aplicar este enfoque ayuda a que nuestros widgets sean más confiables, testables y fáciles de extender.

Código completo en: https://dartpad.dev/?id=359bd243739b2da2c07a3c48575073b2