As with Angular, reactive forms are now on Flutter !
By using flutter_forms, you will be able to simplify your code and validation of you forms.
- Getting Started
- FormBuilder
- How to add a reactive form into an application?
- How to add a reactive form with multiple steps into an application?
- What about validators?
- Providers and Consumers
- See more examples
flutter_forms is very easy to use. First, you should learn to use Flutter.
Please, read the online documentation before using this library.
You will find tutorials, code labs, sample projects... Everything you need to be autonomous.
- Dart SDK: >=2.7.0 <3.0.0
- Flutter: >= 1.17.0
You must install all these dependencies to use flutter_forms :
dependencies:
flutter:
sdk: flutter
build_runner: ^1.12.2
reflectable: ^3.0.0
flutter_forms: ^1.0.0
Then, run flutter packages get command on the console.
flutter_forms is inspired by Angular reactive forms.
Of course, FormBuilder is the starting point of form creation.
How to build a form :
ReactiveFormBuilder form_builder = new ReactiveFormBuilder(
group: new FormGroup(
controls: {
'first_name': new FormControl<String>(value: null, validators: []),
'last_name': new FormControl<String>(value: null, validators: []),
},
validators: [],
),
);
A form is created and automatically instantiated when you add it to ReactiveForm widget. It provides a complete tree of form elements to use.
It's a simplified version of Angular reactive forms. As for Angular reactive forms, flutter_forms can create dynamic forms. Lets see all these points together !
First you have the FormGroup. This one is a group of FormGroup, FormArray and FormControl.
You must use them for complex forms, with multiple levels.
Imagine you have a to set your profile. You could have two distinct parts into this form.
ReactiveFormBuilder form_builder = new ReactiveFormBuilder(
group: new FormGroup(
controls: {
'personal': new FormGroup(
controls: {
'first_name': new FormControl<String>(value: null, validators: []),
'last_name': new FormControl<String>(value: null, validators: []),
},
validators: [],
),
'social_links': new FormGroup(
controls: {
'github': new FormControl<String>(value: null, validators: []),
'facebook': new FormControl<String>(value: null, validators: []),
},
validators: [],
)
},
validators: [],
),
);
How to add a sub part of you form dynamically ?
How to add a FormGroup in the children collection of a FormGroup :
FormGroup root = new FormGroup(controls: {}, validators: []);
FormGroup child = new FormGroup(controls: {}, validators: []);
root.addControl('child', child);
How to add a FormArray in the children collection of a FormGroup :
FormGroup root = new FormGroup(controls: {}, validators: []);
FormArray child = new FormArray(groups: [], validators: []);
root.addControl('child', child);
How to add a FormControl in the children collection of a FormGroup :
FormGroup root = new FormGroup(controls: {}, validators: []);
FormControl<String> child = new FormControl<String>(value: null, validators: []);
root.addControl('child', child);
How to remove a FormGroup from the children collection of a FormGroup :
FormGroup root = new FormGroup(
controls: {
'child': new FormGroup(controls: {}, validators: []),
},
validators: [],
);
root.removeControl('child');
That function will trigger the validation engine and update the form to display errors if there are.
How to remove a FormArray from the children collection of a FormGroup :
FormGroup root = new FormGroup(
controls: {
'child': new FormArray(groups: [], validators: []),
},
validators: [],
);
root.removeControl('child');
That function will trigger the validation engine and update the form to display errors if there are.
How to remove a FormControl from the children collection of a FormGroup :
FormGroup root = new FormGroup(
controls: {
'child': new FormControl<String>(value: null, validators: []),
},
validators: [],
);
root.removeControl('child');
That function will trigger the validation engine and update the form to display errors if there are.
How to check if a control does exist into a FormGroup ?
FormGroup root = new FormGroup(controls: {}, validators: []);
bool exists = root.containsControl('child');
How to get a FormGroup child ?
DON'T DO THIS !
FormGroup root = new FormGroup(
controls: {
'child': new FormGroup(controls: {}, validators: []),
},
validators: [],
);
FormGroup child = root.controls['child'] as FormGroup;
DO THIS !
FormGroup root = new FormGroup(
controls: {
'child': new FormGroup(controls: {}, validators: []),
},
validators: [],
);
FormGroup child = root.getFormGroup('child');
How to get a FormArray child ?
DON'T DO THIS !
FormGroup root = new FormGroup(
controls: {
'child': new FormControl<String>(value: null, validators: []),
},
validators: [],
);
FormControl<String> child = root.controls['child'] as FormControl<String>;
DO THIS !
FormGroup root = new FormGroup(
controls: {
'child': new FormControl<String>(value: null, validators: []),
},
validators: [],
);
FormControl<String> child = root.getFormControl<String>('child');
How to get a FormControl child ?
DON'T DO THIS !
FormGroup root = new FormGroup(
controls: {
'child': new FormArray(groups: [], validators: []),
},
validators: [],
);
FormArray child = root.controls['child'] as FormArray;
DO THIS !
FormGroup root = new FormGroup(
controls: {
'child': new FormArray(groups: [], validators: []),
},
validators: [],
);
FormArray child = root.getFormArray('child');
Next you have the FormArray. This one is a collection of FormGroup only.
This a difference with Angular's library. I disagree with the fact a FormArray can contain directly FormControl items.
In my opinion, Angular FormArray is too permissive. Developer could try to add FormGroup and FormControl items into the same FormArray.
This should not be possible, even if an exception is thrown after.
How to declare a FormArray :
ReactiveFormBuilder form_builder = new ReactiveFormBuilder(
group: new FormGroup(
controls: {
'social_links': new FormArray(
groups: [
new FormGroup(
controls: {
'social_network': new FormControl<String>(value: 'github', validators: []),
'url': new FormControl<String>(value: 'https://github.com/maxime-aubry/', validators: []),
},
validators: [],
),
],
validators: [],
)
},
validators: [],
),
);
As you can see, you can register complex data into an item of a FormArray.
Add an item to a FormArray is easy. Remember that you can add FormGroup items only.
How to add an item :
FormArray array = new FormArray(groups: [], validators: []);
FormGroup child = new FormGroup(controls: {}, validators: []);
array.addGroup(child);
That function will trigger the validation engine and update the form to display errors if there are.
How to remove an item :
FormArray array = new FormArray(
groups: [
// an item is here
],
validators: [],
);
FormGroup child = new FormGroup(controls: {}, validators: []);
array.removeGroup(child);
That function will trigger the validation engine and update the form to display errors if there are. This part is a little confuse because you don't see how to get an item from the form array. Lets see that later.
What would be a form if we didn't use FormControl ?
This is the way to store data, while FormGroup and FormArray are used for the structure !
FormControl are done to support a limited list of data types :
- DateTime.
- num (Number).
- int.
- double.
- String.
- bool.
- List of DateTime.
- List of num.
- List of int.
- List of double.
- List of String.
- List of bool.
- Uint8List, Uint16List, Uint32List, Uint64List, Int8List, Int16List, Int32List and Int64List (for buffer arrays).
- enums.
- list of enums.
If you try to use a disallowed type, an exception will be thrown.
This list could evolve later.
How to declare a FormControl with String generic type :
ReactiveFormBuilder form_builder = new ReactiveFormBuilder(
group: new FormGroup(
controls: {
'first_name': new FormControl<String>(value: 'Maxime', validators: []),
},
validators: [],
),
);
How to set a value to a FormControl :
FormControl<String> control = new FormControl<String>(value: null, validators: []);
control.setValue('my value');
That function will trigger the validation engine and update the form to display errors if there are.
Clone a form element is a very important point of flutter_forms.
Imagine you are modifying an item of your form, as a sub FormGroup. You changes values, and you decide to click on cancel button.
Rollback your values could be hard !
So, you should clone the part of the form you want to update, and apply those changes later if you want.
When you clone a form element, you clone the full tree of ReactiveFormBuilder, so you can use you validators to compare a value with another.
Here is an example :
ReactiveFormBuilder form_builder = new ReactiveFormBuilder(
group: new FormGroup(
controls: {
'first_name': new FormControl<String>(value: 'Maxime', validators: []),
},
validators: [],
),
);
FormControl<String> child = form_builder.group.getFormControl<String>('first_name');
FormControl<String> clone = child.getClone();
Whether for FormGroup, FormArray or FormControl, you can use the same getClone() method.
Now we studied the basics of flutter_forms, lets see how to create a form into a Flutter application.
First, you should define a model file.
This one will contain all you enums, if you will use them.
Into the ./lib/models.dart file, here are the different steps we will do :
- define a namespace for models.
- import flutter_forms.
- define a main() method (for reflectable).
- define you enums.
If you don't define your enums here, they will be refused with FormControl later. An exception will be thrown.
Please, use @flutterFormsValidator notation to declare the content.
@flutterFormsValidator
library example.models;
import 'package:flutter_forms/flutter_forms.dart';
void main() {}
@flutterFormsValidator
enum EGender { male, female }
Use this command line to get the file to get a new file named models.reflectable.dart into a Flutter application project.
> flutter pub run build_runner build
Next, into the main.dart file, you must initialize the namespace to import.
Here, you import example.models from ./lib/models.reflectable.dart.
import 'package:example/models.reflectable.dart';
import 'package:flutter_forms/flutter_forms.dart';
void main() {
initializeReflectable();
LibraryInitializer.initialize(libraryName: 'example.models');
runApp(new MyApp());
}
Here we are. We are going to define our new form.
Into your widget, start by defining a [ReactiveForm] object.
[formBuilder] property receives the ReactiveFormBuilder.
@override
Widget build(BuildContext context) {
return ReactiveForm(
formBuilder: this._getFormBuilder(),
builder: (context, _) {
return new Container();
},
);
}
ReactiveFormBuilder _getFormBuilder() => new ReactiveFormBuilder(
group: new FormGroup(
controls: {
'first_name': new FormControl<String>(value: 'Maxime', validators: []),
'last_name': new FormControl<String>(value: 'AUBRY', validators: []),
},
validators: [],
),
);
We just created a new form with two fields, first_name and last_name.
Next, let add inputs to display on our screen.
@override
Widget build(BuildContext context) {
return ReactiveForm(
formBuilder: this._getFormBuilder(),
builder: (context, _) {
// here, we get the root level of the form builder.
FormGroup root = context.watchFormGroup();
return new Scaffold(
appBar: new AppBar(title: Text("Reactive form")),
body: new Padding(
padding: EdgeInsets.all(5.0),
child: new Column(
children: [
// here, we add inputs for first_name and last_name fields
this._inputText(root.getFormControl<String>('first_name'), 'first name'),
this._inputText(root.getFormControl<String>('last_name'), 'last name'),
],
),
),
floatingActionButton: new FloatingActionButton(
child: Icon(Icons.done),
onPressed: () async {
// here, we get the form state and validate the form
ReactiveFormState formState = context.readFormState();
if (await formState.validate()) {
// Data treatment and post to server here...
}
},
),
);
},
);
}
Widget _inputText(FormControl<String> formControl, String label) =>
new TextFormField(
decoration: InputDecoration(labelText: label),
keyboardType: TextInputType.text,
controller: new TextEditingController(text: formControl.value),
// here, we set the value into the FormControl
onChanged: (String value) async => await formControl.setValue(value),
// here, we set the value into the FormControl
onSaved: (String value) async => await formControl.setValue(value),
// here, we display the error if there is one
validator: (String value) => formControl.error?.message,
);
flutter_forms supports forms with multiples steps !
You must add your forms into a container that will assemble all ReactiveFormState.
Lets see an easy example.
Here is the main page :
@override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(title: Text("Reactive multiple steps form")),
body: new MultipleStepFormContainer(
builder: (context, _) {
return new Stepper(
onStepContinue: () async {
// here we get all steps names
List<String> stepsNames =
context.readMultipleStepFormStateIndexer().keys.toList();
// here we get the form state for the targeted step name
ReactiveFormState formState = context.readFormState(
step: stepsNames[currentStep],
);
// here we validate the current step
if (await formState.validate()) {
// Data treatment and post to server here...
}
},
);
},
),
);
}
Here is a step :
@override
Widget build(BuildContext context) {
return new ReactiveForm(
step: 'profile', // here we define the step name
formBuilder: this._getFormBuilder(),
builder: (context, _) {
FormGroup root = context.watchFormGroup();
// form content here
return new Container();
},
);
}
ReactiveFormBuilder _getFormBuilder() => new ReactiveFormBuilder(
group: new FormGroup(
controls: {
'first_name': new FormControl<String>(value: null, validators: []),
'last_name': new FormControl<String>(value: null, validators: []),
},
validators: [],
),
);
Here is another step :
@override
Widget build(BuildContext context) {
return new ReactiveForm(
step: 'social_links', // here we define the step name
formBuilder: this._getFormBuilder(),
builder: (context, _) {
FormGroup root = context.watchFormGroup();
// form content here
return new Container();
},
);
}
ReactiveFormBuilder _getFormBuilder() => new ReactiveFormBuilder(
group: new FormGroup(
controls: {
'social_links': new FormArray(groups: [], validators: []),
},
validators: [],
),
);
What would be this library without validators ? NOTHING !
So, here is a list of available validators :
Common :
Validator | Description | Progress |
---|---|---|
Required | Validate a FormGroup, FormArray and FormControl. Checks if a FormGroup is not null and contains controls. Checks if a FormArray is not null and contains items. Checks if a FormControl is not null. If this one is a string, checks if it's not an empty string. | done |
FormGroup :
There are not validators for FormGroup for this time.
FormArray :
Validator | Description | Progress |
---|---|---|
NbItems | Checks if a FormArray has a valid length. | done |
FormControls :
Validator | Description | Progress |
---|---|---|
Checks if a string value is a valid email. | done | |
EqualToDateTime | Checks if a datetime value is equal to another. | done |
EqualToDouble | Checks if a double value is equal to another. | done |
EqualToInt | Checks if a int value is equal to another. | done |
EqualToNumber | Checks if a number value is equal to another. | done |
EqualToString | Checks if a string value is equal to another. | done |
FileMimeType | Checks if a file has an allowed mime type. | done |
FileSize | Checks if a file has an allowed size. | done |
GreaterOrEqualToDateTime | Checks if a datetime value is greater or equal to another. | done |
GreaterOrEqualToDouble | Checks if a double value is greater or equal to another. | done |
GreaterOrEqualToInt | Checks if a int value is greater or equal to another. | done |
GreaterOrEqualToNumber | Checks if a number value is greater or equal to another. | done |
GreaterOrEqualToString | Checks if a string value is greater or equal to another. | done |
GreaterThanDateTime | Checks if a datetime value is greater than another. | done |
GreaterThanDouble | Checks if a double value is greater than another. | done |
GreaterThanInt | Checks if a int value is greater than another. | done |
GreaterThanNumber | Checks if a number value is greater than another. | done |
GreaterThanString | Checks if a string value is greater than another. | done |
ImageSize | Checks if the image width and height are valid. | done |
InText | Checks if the text is contained into another text. | done |
MembershipPassword | Checks if the password has a good format according to the settings. | done |
MultiSelect | Checks if value is a selection of items contained into a list of items. | done |
NbValues | Checks if a array value has a valid length. | done |
NotEqualToDateTime | Checks if a datetime value is not equal to another. | done |
NotEqualToDouble | Checks if a double value is not equal to another. | done |
NotEqualToInt | Checks if a int value is not equal to another. | done |
NotEqualToNumber | Checks if a number value is not equal to another. | done |
NotEqualToString | Checks if a string value is not equal to another. | done |
RangeOfDateTime | Checks if a datetime value is between min and max values. | done |
RangeOfDouble | Checks if a double value is between min and max values. | done |
RangeOfInt | Checks if a int value is between min and max values. | done |
RangeOfNumber | Checks if a number value is between min and max values. | done |
RangeOfString | Checks if a string value is between min and max values. | done |
RegularExpression | Checks if a value has a good format according to a regular expression. | done |
SingleSelect | Checks if value is an item contained into a list of items. | done |
SmallerOrEqualToDateTime | Checks if a datetime value is smaller or equal to another. | done |
SmallerOrEqualToDouble | Checks if a double value is smaller or equal to another. | done |
SmallerOrEqualToInt | Checks if a int value is smaller or equal to another. | done |
SmallerOrEqualToNumber | Checks if a number value is smaller or equal to another. | done |
SmallerThanString | Checks if a string value is smaller or equal to another. | done |
SmallerThanDateTime | Checks if a datetime value is smaller than another. | done |
SmallerThanDouble | Checks if a double value is smaller than another. | done |
SmallerThanInt | Checks if a int value is smaller than another. | done |
SmallerThanNumber | Checks if a number value is smaller than another. | done |
SmallerThanString | Checks if a string value is smaller than another. | done |
StringLength | Checks if a string value is a valid length. | done |
Url | Checks if a value has a good URL format.. | done |
How can we add validators to a form element ?
You can add these functions to FormGroup, FormArray and FormControls.
These three classes have a validators property.
So, let see how to do this.
ReactiveFormBuilder _getFormBuilder() => new ReactiveFormBuilder(
group: new FormGroup(
controls: {
'first_name': new FormControl<String>(
value: 'Maxime',
validators: [
Required(error: 'first name is required'),
StringLength(min: 3, max: 50, error: 'first name must have between 3 and 50 characters.')
],
),
'last_name': new FormControl<String>(
value: 'AUBRY',
validators: [
Required(error: 'last name is required'),
StringLength(min: 3, max: 50, error: 'last name must have between 3 and 50 characters.')
],
),
},
validators: [],
),
);
Next time you will validate the form, these validators will be run.
Be careful, these validators will be run in this the order you will add them.
So, don't add StringLength validator before Required validator for example.
You easily can create you own validators.
Custom validators must override one of these four classes :
- FormGroupValidatorAnnotation for FormGroup validators.
- FormArrayValidatorAnnotation for FormArray validators.
- FormControlValidatorAnnotation for FormControl validators.
- FormValidatorAnnotation for validators that are common for these three form elements.
Here is a basic example for a FormGroup validator :
class CustomValidator extends FormGroupValidatorAnnotation {
const CustomValidator({
@required String error,
}) : super(error: error);
@override
Future<bool> isValid(FormGroup control) async {
// TODO: implement isValid
throw UnimplementedError();
}
}
Here is a basic example for a FormArray validator :
class CustomValidator extends FormArrayValidatorAnnotation {
const CustomValidator({
@required String error,
}) : super(error: error);
@override
Future<bool> isValid(FormArray control) async {
// TODO: implement isValid
throw UnimplementedError();
}
}
For FormControl validators, you must use generic type !
Here is a basic example for a FormControl validator :
class CustomValidator extends FormControlValidatorAnnotation<String> {
const CustomValidator({
@required String error,
}) : super(error: error);
@override
Future<bool> isValid(FormControl<String> control) async {
// TODO: implement isValid
throw UnimplementedError();
}
}
flutter_forms uses Provider library to provide and consume form elements into your widgets.
For almost each provider of flutter_forms, you can use Consumers (they are widgets), watchers and readers.
If you want to use Consumers, watchers or readers, data must be provided before.
Watchers are done to get form elements and rebuild widgets than use them when their value changes.
Readers are done to get form elements without rebuilding widgets than use them when their value changes.
Consumers can make reading code more difficult.
The last thing to know is watchers and Consumers do exactly the same thing.
ReactiveFormProvider is used inside ReactiveForm widget. Thanks to him, you are able to use Consumers, watchers and readers without defining it yourself.
Except if you need to go to a new route, you will must share elements with the new widget.
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => new FormProvider(
providers: [
new FormGroupProvider.value(value: existingFormGroup),
],
child: new Destination(),
),
),
);
ReactiveFormStateProvider is used inside ReactiveForm widget. Thanks to him, you are able to use Consumers, watchers and readers without defining it yourself.
ReactiveFormState watcher :
ReactiveFormState formState = context.watchFormState();
ReactiveFormState reader :
ReactiveFormState formState = context.readFormState();
ReactiveFormState consumer :
child: new ReactiveFormStateConsumer(
builder: (context, formState, child) {
return new Container();
},
);
FormGroupProvider is used inside ReactiveForm widget. Thanks to him, you are able to use Consumers, watchers and readers without defining it yourself.
But, if you want to consume a sub form element, in the sub level of the root, you must provide it.
For example, here is a form. Root level is provided, thanks to ReactiveForm widget.
If we want to use Consumers, watchers or readers on a sub form element, use FormGroupProvider.value on it !
@override
Widget build(BuildContext context) {
return ReactiveForm(
formBuilder: this._getFormBuilder(),
builder: (context, _) {
// here, we get the root level of the form builder.
FormGroup root = context.watchFormGroup();
// here, we provide the child FormGroup
return new FormGroupProvider.value(
value: root.getFormGroup('child'),
builder: (context, _) {
FormGroup child = context.watchFormGroup();
return new Container();
}
);
},
);
}
FormGroup watcher :
FormGroup formGroup = context.watchFormGroup();
FormGroup reader :
FormGroup formGroup = context.readFormGroup();
FormGroup consumer :
child: new FormGroupConsumer(
builder: (context, formGroup, child) {
return new Container();
},
);
FormArray is always a sub form element of root level. If you want to consume it, you must provide it.
For example, here is a form. Root level is provided, thanks to ReactiveForm widget.
If we want to use Consumers, Watchers or Readers on a sub form element, use FormArrayProvider.value on it !
@override
Widget build(BuildContext context) {
return ReactiveForm(
formBuilder: this._getFormBuilder(),
builder: (context, _) {
// here, we get the root level of the form builder.
FormGroup root = context.watchFormGroup();
// here, we provide the child FormArray
return new FormArrayProvider.value(
value: root.getFormArray('child'),
builder: (context, _) {
FormArray child = context.watchFormArray();
return new Container();
}
);
},
);
}
FormArray watcher :
FormArray formArray = context.watchFormArray();
FormArray reader :
FormArray formArray = context.readFormArray();
FormArray consumer :
child: new FormArrayConsumer(
builder: (context, formArray, child) {
return new Container();
},
);
FormControl is always a sub form element of root level. If you want to consume it, you must provide it.
For example, here is a form. Root level is provided, thanks to ReactiveForm widget.
If we want to use Consumers, Watchers or Readers on a sub form element, use FormControlProvider.value on it !
@override
Widget build(BuildContext context) {
return ReactiveForm(
formBuilder: this._getFormBuilder(),
builder: (context, _) {
// here, we get the root level of the form builder.
FormGroup root = context.watchFormGroup();
// here, we provide the child FormControl
return new FormControlProvider.value(
value: root.getFormControl<String>('child'),
builder: (context, _) {
FormControl<String> child = context.watchFormControl<String>();
return new Container();
}
);
},
);
}
FormControl watcher :
FormControl<String> formControl = context.watchFormControl<String>();
FormControl reader :
FormControl<String> formControl = context.readFormControl<String>();
FormControl consumer :
child: new FormControlConsumer<String>(
builder: (context, formControl, child) {
return new Container();
},
);
MultipleStepFormStateIndexer is used only inside form with multiple steps.
Its role is to assemble all ReactiveFormState inside an indexer. So, Stepper will be able to target the good form state that you should use.
Use it when you want to validate a step :
@override
Widget build(BuildContext context) {
return new MultipleStepFormContainer(
builder: (context, _) {
return new Stepper(
type: StepperType.horizontal,
steps: this.steps,
currentStep: currentStep,
onStepContinue: () async {
// here we get all steps names
List<String> stepsNames =
context.readMultipleStepFormStateIndexer().keys.toList();
// here we get the form state for the targeted step name
ReactiveFormState formState = context.readFormState(
step: stepsNames[currentStep],
);
// here we validate the current step
if (await formState.validate()) {
// Data treatment and post to server here...
}
},
onStepTapped: (step) => goTo(step),
onStepCancel: cancel,
);
},
);
}
FormControl watcher :
MultipleStepFormStateIndexer indexer = context.watchMultipleStepFormStateIndexer();
FormControl reader :
MultipleStepFormStateIndexer indexer = context.readMultipleStepFormStateIndexer();
I invite you to study example project.
Example project uses libraries :