flutter_formy 0.3.0
flutter_formy: ^0.3.0 copied to clipboard
Form management package focused on flexibility, modularity, and readability
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(
field: group.field('email'),
decoration: (fieldState, firstError) => InputDecoration(
hintText: 'Digite seu E-mail',
labelText: 'E-mail',
errorText: firstError,
),
),
FormyTextField(
field: 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(
field: group.field('email'),
decoration: (fieldState, firstError) => InputDecoration(
hintText: 'Digite seu E-mail',
labelText: 'E-mail',
errorText: firstError,
),
),
const SizedBox(height: 24),
FormyTextField(
field: 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',
),
),
],
);
}
}
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): #
Os validadores servem pra validar os campos. Um campo pode ter de 1 a any validadores. Toda vez que o valor é atualizado, os validadores vão ser chamados. Após a validação, ele retorna a classe ValidationResult que contem a key do validador e uma flag determinando se é ou não valido.
O package já vem com alguns validadores ja definidos pra usar, são eles:
IsRequired: Valida se o campo não esta vazio ou null;EmailValidator: Valida se o valor do campo é um email;MaxValidator: Valida se o tamanho do campo, seja String ou List, ultrapassou um limite maximo definido;MinValidator: Valida se o tamanho do campo, seja String ou List, ultrapassou um limite mínimo definido;MustMatchValidator: Valida se dois campos são iguais.
Observação: Mais validadores vão vir, esses são apena os primeiros.
Criando um validador customizado #
Para criar um validador o processo é muito facil. Primeiro crie uma classe, depois estenda ela de FormyValidator e pronto. O método onValidate é obrigatório e é ele que vai fazer a validação.
Criar um validador no Formy é simples. Basta criar uma classe que estenda FormyValidator e implementar o método onValidate, responsável por verificar se o valor do campo é válido ou nã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.
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 statustouchedfor 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
keynã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 comodirtymanualmente;markAsTouched: Marca o campo comotouchedmanualmente. 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
getsã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 validadorIsRequiredna lista de validadores;firstError: Pega a mensagem do primeiroValidationResultnão valido;errorKeys: Pega todas as keys dosValidationResultnão validos, util pra debug;errorMessages: Pega todas as mensagens dosValidationResultnão validos;valid: Pega um booleanos que identifica se o campo é valido, ou seja, quando todos oValidationResultforem 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;FieldConfigserve pra instanciarFieldControllerdentro do grupo. Ele tem as mesmas propriedades doFieldController+dependsOn, que serve pra determinar quais campos (keydos campos), do mesmo grupo, esse campo é dependente;
subGroups: Lista deSubGroupConfig.SubGroupConfigserve pra instanciarGroupControllercomo 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 aoFieldControllerdakeymencionado 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
keynão pode conter “/”, esse caractere separa akeydo subgrupo dakeydo 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 oFieldControllerdo grupo;field: Pega um campo especifico do grupo usando akeydo campo;subGroup: Pega um subgrupo especifico do grupo usando akeydo 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 comotouchede 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
getsã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 akeydele;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: OFieldControllerque 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 doFieldControllermudar;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:BuildContextdo widget na árvore Flutter;field:FieldControllerque vai manipular;child: Widget fixo definido na propriedadechilddoFieldBuilder.
class ColorDotPickerField extends FieldBuilder<Color> {
ColorDotPickerField({
super.key,
required super.field,
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 deFieldBuildere tem a mesma funcionalidade doFieldBuilder, mas focado em campos em que o foco é importante. Ele tem a propriedadefocusNode, que é do tipoFocusNode. Essa propriedade é passado probuildercomo 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: É umTextFormFieldadaptado pro Formy; -
FormyDropdown: É umDropdownMenuadaptado pro Formy; -
FormyCheckbox: É umCheckboxadaptado pro Formy; -
FormyListCheckbox: Serve pra criar uma lista deCheckbox. A propriedadeitemsEntrydefine o texto e o valor do checkbox, elayoutdefine 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),
)
],
);
}
}