Formy

ATENÇÃO:

  • O package esta em fase beta. Ele esta funcional, mas tem alguns pontos a ser melhorado pra melhorar a experiencia do desenvolvedor;
  • O README ainda não esta completo, mas em pouco tempo vai estar;
  • An English readme will be created in the future.

Sobre o Formy:

Formy é uma biblioteca robusta para gerenciamento de formulários em Flutter. Ela simplifica a criação, o controle e a validação de formulários, oferecendo uma abordagem reativa que mantém a interface sincronizada com o estado dos campos em tempo real. Com Formy, você pode construir formulários complexos de forma organizada, reutilizável e fácil de manter, aproveitando recursos como controle granular de estado, validação customizada e construção dinâmica dos campos.

pub package License

Instalando:

Adicione o Formy no arquivo pubspec.yaml

dependencies:
  flutter_formy:

Criando um formulário:

Pra criar um formulário é preciso usar a classe GroupController. Nele a gente define a key e os campos (fields):

final GroupController group = GroupController(
    key: 'login',
    fields: [
      FieldConfig<String>(key: 'email', validators: [IsRequired()]),
      FieldConfig<String>(key: 'password', validators: [IsRequired(), MinValidator(6)]),
    ],
  );

Depois de definir, crie o widget pro formulário, deixe o visual como quiser. Pra usar os campos definidos no fields, é preciso usar um widget FieldBuilder (exemplo: FormyTextField):

FormyTextField(
  controller: group.field('email'),
  decoration: (fieldState, firstError) => InputDecoration(
    hintText: 'Digite seu E-mail',
    labelText: 'E-mail',
    errorText: firstError,
  ),
),
FormyTextField(
  controller: group.field('password'),
  decoration: (fieldState, firstError) => InputDecoration(
    hintText: 'Digite sua senha',
    labelText: 'Senha',
    errorText: firstError,
  ),
),

Pra fazer um botão que reaja as mudanças do GroupController, é preciso usar um widget FormySubmitButton:

FormySubmitButton(
  control: group,
  child: const Text(
    'Entrar',
  ),
),

O pronto, o formulario esta feito, esta validando e o botão esta reagindo a mudança de estado. Um exemplo completo de uma tela de login esta logo a baixo:

class LoginPage extends StatefulWidget {
  const LoginPage({super.key});

  @override
  State<LoginPage> createState() => _LoginPageState();
}

class _LoginPageState extends State<LoginPage> {
  final GroupController group = GroupController(
    key: 'login',
    fields: [
      FieldConfig<String>(key: 'email', validators: [IsRequired()]),
      FieldConfig<String>(key: 'password', validators: [IsRequired(), MinValidator(6)]),
    ],
  );

  @override
  void dispose() {
    super.dispose();
    group.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        Text(
          'Login',
          style: Theme.of(context).textTheme.headlineSmall,
        ),
        const SizedBox(height: 30),
        FormyTextField(
          controller: group.field('email'),
          decoration: (fieldState, firstError) => InputDecoration(
            hintText: 'Digite seu E-mail',
            labelText: 'E-mail',
            errorText: firstError,
          ),
        ),
        const SizedBox(height: 24),
        FormyTextField(
          controller: group.field('password'),
          decoration: (fieldState, firstError) => InputDecoration(
            hintText: 'Digite sua senha',
            labelText: 'Senha',
            errorText: firstError,
          ),
        ),
        const SizedBox(height: 30),
        FormySubmitButton(
          control: group,
          child: const Text(
            'Entrar',
          ),
        ),
      ],
    );
  }
}



Criando um formulário com FormyForm

Se você quiser evitar boilerplate (instanciar e descartar GroupController manualmente), pode herdar de FormyForm. Essa classe já cuida de criar e dar dispose no controller automaticamente, você só precisa declarar os campos e montar a interface com formBody.

class LoginForm extends FormyForm {
  const LoginForm({super.key});

  @override
  List<FieldConfig<dynamic>> fields() => [
        FieldConfig<String>(key: 'email', validators: [IsRequired()]),
        FieldConfig<String>(key: 'password', validators: [IsRequired(), MinValidator(6)]),
      ];

  @override
  Widget formBody(BuildContext context, GroupController controller) {
    return Column(
      children: [
        FormyTextField(
          controller: controller.field('email'),
          decoration: (fieldState, firstError) => InputDecoration(
            labelText: 'E-mail',
            hintText: 'Digite seu e-mail',
            errorText: firstError,
          ),
        ),
        const SizedBox(height: 20),
        FormyTextField(
          controller: controller.field('password'),
          decoration: (fieldState, firstError) => InputDecoration(
            labelText: 'Senha',
            hintText: 'Digite sua senha',
            errorText: firstError,
          ),
        ),
        const SizedBox(height: 30),
        FormySubmitButton(
          control: controller,
          child: const Text('Entrar'),
        ),
      ],
    );
  }
}

Vantagens do FormyForm

  • Não precisa instanciar GroupController manualmente.
  • O dispose do controller já é chamado automaticamente.
  • A estrutura fica mais clara: fields() define a configuração dos campos, formBody() define a UI.

Recursos:

  • Validação automática e customizável
  • Agrupamento de campos via FieldGroup
  • Validação reativa e baseada em contexto
  • Geração dinâmica de campos com FormSchema (em breve)
  • Gerenciamento escopado com FormyScope (em breve)

Validadores (FormyValidator):

No Flutter Formy, validadores são classes responsáveis por garantir que o valor de um campo atenda certas regras.
Eles são usados junto aos FieldControllers para definir restrições como tamanho mínimo, formatos específicos (ex: email, URL), ou condições que dependem de outros campos (ex: confirmar senha).

Quando o valor do campo não passa pela validação, o validador retorna uma mensagem de erro que pode ser exibida automaticamente no seu formulário.
Isso torna o processo de construção de formulários dinâmicos, reativos e seguros muito mais simples.

Validadores disponíveis

O package já vem com diversos validadores prontos para usar, são eles:

Comprimento (Strings / Lists / Map / Set)

  • MinLengthValidator: Valida se o tamanho do campo é pelo menos o mínimo especificado.
  • MaxLengthValidator: Valida se o tamanho do campo é no máximo o máximo especificado.
  • BetweenLengthValidator: Valida se o tamanho do campo está dentro de um intervalo específico.
  • ExactLengthValidator: Valida se o tamanho do campo é exatamente o especificado.

Obrigatoriedade

  • IsRequired: Valida se o campo não está vazio, null, ou false.

Combinadores

  • OrValidator: Valida se ao menos um dos validadores passados for válido.

Conteúdo de Strings / Lists / Map

  • ContainsValidator: Valida se o campo contém um valor específico.
  • NotContainsValidator: Valida se o campo não contém um valor específico.

Valores numéricos

  • BetweenValuesValidator: Valida se o valor numérico está dentro de um intervalo.
  • DivisibleByValidator: Valida se o valor é divisível por um número específico.
  • EvenNumValidator: Valida se o número é par.
  • OddNumValidator: Valida se o número é ímpar.
  • MaxValueValidator: Valida se o valor é no máximo o especificado.
  • MinValueValidator: Valida se o valor é no mínimo o especificado.
  • NegativeNumValidator: Valida se o número é negativo.
  • PositiveNumValidator: Valida se o número é positivo.
  • NonZeroValidator: Valida se o número não é zero.

Datas e Idades

  • AfterDateValidator: Valida se a data é depois da data especificada.
  • BeforeDateValidator: Valida se a data é antes da data especificada.
  • BetweenDatesValidator: Valida se a data está dentro de um intervalo de datas.
  • MaxAgeValidator: Valida se a idade calculada pela data não ultrapassa o máximo.
  • MinAgeValidator: Valida se a idade é pelo menos o mínimo especificado.

Cross-field (campos dependentes)

  • BiggerThanValidator: Valida se o valor é maior que o valor de outro campo.
  • LessThanValidator: Valida se o valor é menor que o valor de outro campo.
  • MustMatchValidator: Valida se o valor é igual ao valor de outro campo (ex: senha e confirmação).
  • MustNotMatchValidator: Valida se o valor é diferente do valor de outro campo.

Validações específicas de String

  • EmailValidator: Valida se o campo é um email válido.
  • UrlValidator: Valida se o campo é uma URL válida.
  • IpValidator: Valida se o campo é um IP válido.
  • PatternValidator: Valida se o campo bate com um regex específico.
  • StartWithValidator: Valida se o campo começa com um valor específico.
  • EndWithValidator: Valida se o campo termina com um valor específico.
  • NoNumbersValidator: Valida se o campo não contém números.
  • NoSpaceValidator: Valida se o campo não contém espaços.
  • NoSpecialCharsValidator: Valida se o campo não contém caracteres especiais.

Criando um validador customizado

Para criar validadores customizados no Formy, você pode estender as classes base fornecidas pelo package. Existem dois tipos principais de validadores que podem ser implementados:

  • FormyValidator: utilizado para validar campos individualmente, como verificar se um campo está preenchido, se um e-mail é válido, ou se uma senha tem o tamanho mínimo necessário.
  • FormyCrossValidator: utilizado para validação cruzada, ou seja, quando a validação de um campo depende do valor de outro campo, como confirmar se dois campos de senha são iguais ou se uma data de início é anterior à data de término.

A seguir, veja como criar validadores customizados usando essas duas bases.

FormyValidator

Para criar um validador individual, basta criar uma classe que estenda FormyCrossValidator e pronto. O método onValidate é obrigatório e é ele que vai fazer a validação.

Exemplo: validador de número de telefone no padrão E.164 (ex: +14155552671)

class PhoneNumberValidator extends FormyValidator<String> {
  final String message;
  PhoneNumberValidator({
    super.message = 'Número de telefone inválido (ex: +14155552671)',
  });

  @override
  ValidationResult onValidate(FieldController<String> control) {
    final value = control.value?.trim();
    final phoneRegex = RegExp(r'^\+[1-9]\d{1,14}$');
    if (!phoneRegex.hasMatch(value)) {
      return ValidationResult.error(key:'phoneValidator',message:message);
    }

    return ValidationResult.ok(key:'phoneValidator');
  }
}

Observação: A key no ValidationResult serve pra identificar de qual validador esta vindo o resultado, muito útil pra debug.

FormyCrossValidator:

Para criar um validador cruzado, basta criar uma classe que estenda FormyCrossValidator e passar a key do outro campo no construtor. Dentro do método obrigatório onValidate, você pode acessar o outro campo usando o getter otherController.

Exemplo genérico: validador que garante que o valor de um campo seja maior que o valor de outro campo numérico:

class GreaterThanOtherFieldValidator extends FormyCrossValidator<num> {
  GreaterThanOtherFieldValidator({
    required super.otherField,
    super.message = 'O valor deve ser maior que o outro campo',
  });

  @override
  ValidationResult onValidate(FieldController<num> control) {
    final other = otherController(control);
    if (control.value == null || other.value == null) {
      return ValidationResult.ok(key: 'greaterThanOther');
    }
    if (control.value! <= other.value!) {
      return ValidationResult.error(key: 'greaterThanOther', message: message);
    }
    return ValidationResult.ok(key: 'greaterThanOther');
  }
}

No exemplo acima, otherField é a chave do campo que será comparado. O getter otherController retorna o FieldController do outro campo, permitindo acessar seu valor e estado.

Observação: Use FormyCrossValidator sempre que precisar validar dependências entre campos, como confirmação de senha, comparação de datas, valores relacionados, etc.

Controlador de campo (FieldController):

O FieldController serve pra controlar um campo. Ele gerencia os status, o valor, validações e outra informações sobre o campo. Todo FieldController tem uma key, que vai ser usado pra encontrar o campo certo dentro de um GroupController. As propriedades do FieldController são:

  • key: Chave de identificação do campo;
  • validators: Lista de validadores (FormyValidator);
  • initialValue: Valor inicial do campo, quando não definido o valor inicial é null;
  • showErrorWhen: Defini quando mostrar o erro. O valor é um enum(ShowError) e tem 3 opções
    • never: Nunca mostrar;
    • whenIsTouched: Quando o status touched for verdadeiro (valor padrão);
    • always: Mostrar o erro logo em seguida que tiver um erro;
final email = FieldController<String>(
      key: 'email',
      validators: [IsRequired(), EmailValidator()],
    );

Observação:

  • A key não pode conter “/”, esse caractere separa a key do campo da key do grupo em que ele esta;
  • Alguns validadores funcionam apenas com um tipo de valor (exemplo o EmailValidator, só funciona com String). Importante definir o tipo do campo no parâmetro genérico;
  • Quando um FieldController esta fora de um GroupController, ele é considerado um campo independente.

Uma limitação do FieldController é não permitir o uso de valores do tipo List, nesse caso o FieldListControl deve ser usado.

Além das propriedades, FieldController tem alguns métodos quem podem ser usados fora da classe, são eles:

  • validate: Valida todos os campos (ele é chamado automaticamente quando o valor é modificado);
  • update: Atualiza o valor do campo e marca como dirty (pra identificar se já foi modificado). Quando construir um widget de campo (FieldBuilder), é preciso usar esse método pra atualizar o valor do campo;
  • markAsDirty: Marca o campo como dirty manualmente;
  • markAsTouched: Marca o campo como touched manualmente. No FieldBuilder é preciso chamar ele caso queira e/ou precise dessa informação;
  • reset: Reseta o valor do campo pro valor inicial;
  • updateValidators: Atualiza a lista de validadores. Útil campo trabalhar com internacionalização e tem um campo em que a validação muda dependendo do pais;
  • Os método get são:
    • groupRef: Pega o group (GroupController) que ele faz parte;
    • completeKey: Pega a key completa do campo (key do group + key do campo);
    • value: Pega o valor do campo;
    • validationResults: Pega a lista de resultados de validações (ValidationResult), util pra debug;
    • state: Pega a classe de estado do campo (FieldState);
    • isRequired: Pega um booleano que identifica se o campo é obrigatório ou não. Isso é determinado pelo uso do validador IsRequired na lista de validadores;
    • firstError: Pega a mensagem do primeiro ValidationResult não valido;
    • errorKeys: Pega todas as keys dos ValidationResult não validos, util pra debug;
    • errorMessages: Pega todas as mensagens dos ValidationResult não validos;
    • valid: Pega um booleanos que identifica se o campo é valido, ou seja, quando todos o ValidationResult forem validos;

Controlador de campo do tipo lista (FieldListControl):

O FieldListControl tem as mesmas propriedade e métodos do FieldController , mas deve ser usado apenas em campos com valores do tipo List. Essa classe possui 3 métodos exclusivos, são eles:

  • addItem: Adiciona um item no valor do campo;
  • removeItem: Remove um item no valor do campo;
  • moveItem: Move um item do valor de posição;
final interests = FieldListControl(
      key: 'interests',
      validators: [IsRequired(), MinValidator(5), MaxValidator(10)],
    );

Estado do campo (FieldState):

Esse é o estado do FieldListControl. O FieldState tem as propriedades:

  • value: Valor do campo;
  • validationResults: Resultado das validações do campo (lista de ValidationResult);
  • dirty: Determina se o campo foi modificado;
  • touched: Determina se o campo foi tocado:

Controlador de grupo (GroupController)

O GroupController serve pra controlar um grupo de campos. Ele gerencia os status, validações e dependências dos campos. Todo GroupController tem uma key, que serve pra encontrar o grupo no FormManager (será removido futuramente). As propriedades do GroupController são:

  • key: Chave de identificação do grupo;
  • fields: Lista de FieldConfig;
    • FieldConfig serve pra instanciar FieldController dentro do grupo. Ele tem as mesmas propriedades do FieldController + dependsOn, que serve pra determinar quais campos (key dos campos), do mesmo grupo, esse campo é dependente;
  • subGroups: Lista de SubGroupConfig.
    • SubGroupConfig serve pra instanciar GroupController como subgrupo dentro do grupo (grupos aninhados). Ele tem as mesmas propriedade do GroupController + dependsOn, que serve pra determinar quais campos, do mesmo grupo principal, esse subgrupo é dependente. dependsOn é do tipo DependsOn, que tem as propriedades:
      • fieldKey: chave do campo que o subgrupo é dependente;
      • enabledWhen: Função que contem um FieldController, referente ao FieldController da key mencionado na propriedade fieldKey, como parâmetro e retorna um booleano indicando se o subgrupo deve ou não ser ativo. Quando ativo, ele entra na lista de validações do grupo principal. Quando desativo, o grupo principal pode ser valido independente do subgrupo e seus validadores.
final GroupController group = GroupController(
      key: 'community',
      fields: [
        FieldConfig<String>(key: 'name', validators: [IsRequired()]),
        FieldConfig<String>(key: 'email', validators: [IsRequired()]),
        FieldConfig<bool>(key: 'addAddress')
      ],
      subGroups: [
        SubGroupConfig(
          key: 'address',
          dependsOn: [
            DependsOn(
                fieldKey: 'addAddress',
                enabledWhen: (FieldController controller) {
                  return controller.state.value == true;
                })
          ],
          fields: [
            FieldConfig<String>(
                key: 'country', validators: [IsRequired()]),
            FieldConfig<String>(key: 'state', validators: [IsRequired()]),
            FieldConfig<String>(key: 'city', validators: [IsRequired()]),
            FieldConfig<String>(key: 'street'),
          ],
        ),
      ],
    );

Observação:

  • A key não pode conter “/”, esse caractere separa a key do subgrupo da key do grupo principal;
  • Os subgrupos são GroupController, isso faz com que eles tenham as mesmas propriedades e métodos do grupo principal.

GroupController tem uma lista de métodos muito uteis pra trabalhar, são eles:

  • getAllFields: Pega todos o FieldController do grupo;
  • field: Pega um campo especifico do grupo usando a key do campo;
  • subGroup: Pega um subgrupo especifico do grupo usando a key do subgrupo
  • findFieldByCompleteKey: Pega um campo especifico dentro do grupo aninhado. Se usar por exemplo ‘address/country’, ele vai pegar o campo de key ‘country’ que esta no subgrupo de key ‘address’;
  • findSubGroupByCompleteKey: Função semelhante ao findFieldByCompleteKey, mas invés de pegar um campo, ele pega um subgrupo dentro do grupo aninhado;
  • getFormData: Retorna os valores dos campos do grupo e dos campos dos subgrupos formatados em um map. Util pra quando precisa transformar os valores do grupo em uma entidade;
  • resetAll: Reseta os valores dos campos do grupo pros seus valores iniciais;
  • setInitialValues: Define os valores atuais como valores iniciais dos campos do grupos e seus subgrupos. Útil pra quando tem um formulário que após ser salvo, se torna apenas pra leitura;
  • touchAndValidateAllFields: Define todos os campos do grupo e de seus subgrupos como touched e valida eles;
  • dispose: Remove todos os listeners dentro do grupo, útil pra não vazar memoria. Esse método é chamado automaticamente após o grupo não ser mais usado.
  • Os método get são:
    • parentGroup: Pega o grupo que ele faz parte. No grupo principal o método retorna null;
    • completeKey: Retoda a key completa do subgrupo (key do grupo princial + key do subgrupo). No grupo principal retorna apenas a key dele;
    • state: Pega a classe de estado do grupo(GroupState);

Estado do grupo(GroupState)

Esse é o estado do GroupController. O GroupState tem as propriedades:

  • isEnabled: Determina se o grupo esta ativo;
  • isValid: Determina se o grupo esta valido;
  • wasValidated: Determina se o grupo foi validado uma vez;
  • errorMessages: Todos os erros dos campos do grupo:
  • firstErrorField: A mensagem do primeiro erro do primeiro campo não valido do grupo;
  • validCount: Numero de campos validos no grupo.

Criando um widget customizado

Com o Formy é possível criar os mais variados tipos de campos para o formulário, campo de texto, dropdown, checkbox, radio, qualquer tipo, apenas usando o widget FieldBuilder. FieldBuilder é um widget poderoso na criação de campos customizados e com poucas propriedades:

  • field: O FieldController que o widget vai manipular;
  • buildWhen: Uma função que determina quando o widget deve sofrer rebuild. Quando null, ele vai sofrer rebuild com qualquer atualização do FieldController. A função possui dois parâmetros:
    • oldState: Estado antigo do FieldController;
    • currentState: Estado atual do FieldController;
  • child: Serve como um widget fixo ou estático. Ele não vai sofrer rebuild quando o estado do FieldController mudar;
  • builder: É uma função obrigatória que define como renderizar a UI com base no estado do FieldController, sendo o principal ponto de personalização visual. Ele tem 3 parâmetros:
    • context: BuildContext do widget na árvore Flutter;
    • field: FieldController que vai manipular;
    • child: Widget fixo definido na propriedade child do FieldBuilder.
class ColorDotPickerField extends FieldBuilder<Color> {
  ColorDotPickerField({
    super.key,
    required super.controller,
    super.buildWhen,
    this.colors = const [
      Colors.red,
      Colors.green,
      Colors.blue,
      Colors.orange,
      Colors.purple,
    ],
    this.size = 32,
    this.spacing = 8,
  }) : super(
          builder: (context, field, child) {
            return Wrap(
              spacing: spacing,
              children: colors.map((color) {
                final isSelected = field.value == color;
                return GestureDetector(
                  onTap: () => field.update(color),
                  child: AnimatedContainer(
                    duration: const Duration(milliseconds: 150),
                    width: size,
                    height: size,
                    decoration: BoxDecoration(
                      shape: BoxShape.circle,
                      color: color,
                      border: isSelected
                          ? Border.all(color: Colors.black, width: 3)
                          : null,
                    ),
                  ),
                );
              }).toList(),
            );
          },
        );

  final List<Color> colors;
  final double size;
  final double spacing;
}

Observações:

  • Além do FieldBuilder, o Formy possui o FocusableFieldBuilder. É um widget que estende de FieldBuilder e tem a mesma funcionalidade do FieldBuilder, mas focado em campos em que o foco é importante. Ele tem a propriedade focusNode, que é do tipo FocusNode. Essa propriedade é passado pro builder como parâmetro.
// Um campo de texto simples que estende de FocusableFieldBuilder
class SimpleTextInput extends FocusableFieldBuilder {
  final String? label;
  final String? hintText;
  SimpleTextInput ({
    super.key,
    required super.field,
    this.label,
    this.hintText,
  }) : super(
          builder: (context, field, focusNode, child) {
            return TextFormField(
              initialValue: field.initialValue,
              onChanged: field.update,
              focusNode: focusNode,
              style: Theme.of(context).textTheme.bodyMedium,
              decoration: InputDecoration(
                label: Text(label ?? 'Label'),
                errorText: field.firstError,
                hintText: hintText,
              ),
            );
          },
        );
}

Widgets já prontos

Aqueles campos mais usados, mais comuns, já foram criados no Formy pra aumentar sua produtividade. Abaixo esta os campos:

  • FormyTextField: É um TextFormField adaptado pro Formy;

  • FormyDropdown: É um DropdownMenu adaptado pro Formy;

  • FormyCheckbox: É um Checkbox adaptado pro Formy;

  • FormyListCheckbox: Serve pra criar uma lista de Checkbox. A propriedade itemsEntry define o texto e o valor do checkbox, e layout define como vai ficar organizado a lista. Com a propriedade layout é possivel organizar os checkbox em linhas, colunas, wrap, do jeito que você quiser;

    //Exemplo de um FormyListCheckbox
      
    List<String> packagesName = [
    	'Formy', 'Get', 'Dartz', 'Equatable', 'Bloc', 'Slang', 'Dio',
    ];
    return FormyListCheckbox<String>(
    	fieldController: group.field('favoritePackages'),
    	title: Text('Your favorite packages (min 2, max 5)'),
    	itemsEntry: packagesName.map((e) => 
    		ItemEntry(value: e, text: Text(e))).
    		toList(),
    	layout: (List<Widget> children) => Wrap(
    		runSpacing: 5,
    		spacing: 5,
    		children: children,
    	),
    ),
    
  • FormyRadio: Serve pra criar uma lista de Radio. Tem as mesmas propriedades do FormyListCheckbox;

Observações:

  • Todos esses widgets possuem um campo fieldController, é obrigatório pra qualquer widget que utilize FieldBuilder;
  • Mais campos já prontos vai ser criado em atualizações futuras.

Só com esses widgets já é possível criar um formulário, mas se acha que precisa de um detalhe a mais, você pode criar um widget e usar eles como usa qualquer outro widget.

// Exemplo de um widget que usa um widget de campo do Formy

class TextInput extends StatelessWidget {
  const TextInput({
    super.key,
    required this.fieldController,
    this.label,
    this.hintText,
    this.maxLines,
  });
  final FieldController<String> fieldController;
  final String? label;
  final String? hintText;
  final int? maxLines;

  @override
  Widget build(BuildContext context) {
    return Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: <Widget>[
        Text(label ?? 'Text field'),
        FormyTextField(
          fieldController: fieldController,
          maxLines: maxLines,
          decoration: (FieldState<dynamic> fieldState, String? firstError) =>
              InputDecoration(errorText: firstError, hintText: hintText),
        )
      ],
    );
  }
}

Libraries

flutter_formy
Formy - A modular form state management library for Flutter.
flutter_formy_base_validators
A library that defines FormyValidator, FormyCrossValidator and various field-level validators for validating data in FieldControllers.
flutter_formy_builder
A library that defines FormyBuilder, FieldBuilder, FocusableFieldBuilder, and GroupBuilder for building reactive Flutter forms.
flutter_formy_cross_validators
A library that provides cross-field validators for use with Formy.
flutter_formy_date_validators
A library that provides date-based validators for use with Formy.
flutter_formy_generic_validators
A library that provides generic field validators for use with Formy.
flutter_formy_numeric_validators
A library that provides numeric validators for use with Formy.
flutter_formy_selector
A library that provides FormySelector, FieldSelector, and GroupSelector for building reactive Flutter forms. These classes listen to value changes and automatically rebuild widgets when the selected data changes.
flutter_formy_string_validators
A library that provides string pattern validators for use with Formy.