flutter_formy 0.1.0
flutter_formy: ^0.1.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 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.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 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),
)
],
);
}
}