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.
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 FieldController
s 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çõesnever
: Nunca mostrar;whenIsTouched
: Quando o statustouched
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 comodirty
(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 comodirty
manualmente;markAsTouched
: Marca o campo comotouched
manualmente. NoFieldBuilder
é 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 validadorIsRequired
na lista de validadores;firstError
: Pega a mensagem do primeiroValidationResult
não valido;errorKeys
: Pega todas as keys dosValidationResult
não validos, util pra debug;errorMessages
: Pega todas as mensagens dosValidationResult
não validos;valid
: Pega um booleanos que identifica se o campo é valido, ou seja, quando todos oValidationResult
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 deValidationResult
);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 instanciarFieldController
dentro do grupo. Ele tem as mesmas propriedades doFieldController
+dependsOn
, que serve pra determinar quais campos (key
dos campos), do mesmo grupo, esse campo é dependente;
subGroups
: Lista deSubGroupConfig
.SubGroupConfig
serve pra instanciarGroupController
como subgrupo dentro do grupo (grupos aninhados). Ele tem as mesmas propriedade doGroupController
+dependsOn
, que serve pra determinar quais campos, do mesmo grupo principal, esse subgrupo é dependente.dependsOn
é do tipoDependsOn
, que tem as propriedades:fieldKey
: chave do campo que o subgrupo é dependente;enabledWhen
: Função que contem umFieldController
, referente aoFieldController
dakey
mencionado na propriedadefieldKey
, 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 akey
do subgrupo dakey
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 oFieldController
do grupo;field
: Pega um campo especifico do grupo usando akey
do campo;subGroup
: Pega um subgrupo especifico do grupo usando akey
do subgrupofindFieldByCompleteKey
: 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 aofindFieldByCompleteKey
, 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 comotouched
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 akey
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
: OFieldController
que o widget vai manipular;buildWhen
: Uma função que determina quando o widget deve sofrer rebuild. Quandonull
, ele vai sofrer rebuild com qualquer atualização doFieldController
. A função possui dois parâmetros:oldState
: Estado antigo doFieldController
;currentState
: Estado atual doFieldController
;
child
: Serve como um widget fixo ou estático. Ele não vai sofrer rebuild quando o estado doFieldController
mudar;builder
: É uma função obrigatória que define como renderizar a UI com base no estado doFieldController
, 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 propriedadechild
doFieldBuilder
.
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 oFocusableFieldBuilder
. É um widget que estende deFieldBuilder
e tem a mesma funcionalidade doFieldBuilder
, mas focado em campos em que o foco é importante. Ele tem a propriedadefocusNode
, que é do tipoFocusNode
. Essa propriedade é passado probuilder
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
: É umTextFormField
adaptado pro Formy; -
FormyDropdown
: É umDropdownMenu
adaptado pro Formy; -
FormyCheckbox
: É umCheckbox
adaptado pro Formy; -
FormyListCheckbox
: Serve pra criar uma lista deCheckbox
. A propriedadeitemsEntry
define o texto e o valor do checkbox, elayout
define como vai ficar organizado a lista. Com a propriedadelayout
é 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 deRadio
. Tem as mesmas propriedades doFormyListCheckbox
;
Observações:
- Todos esses widgets possuem um campo
fieldController
, é obrigatório pra qualquer widget que utilizeFieldBuilder
; - 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.