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
Imaginemos que tenemos un dropdown o widgets personalizados que queremos validar cuando el usuario presione un botón como en la siguiente imagen:
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:
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.
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'), ), ], ), ); } }
Con este enfoque conseguimos varios objetivos importantes:
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