This package helps in creation of data collection forms in Flutter by removing the boilerplate needed to build a form, validate fields, react to changes, and collect final user input.
To use this plugin, add flutter_form_builder
as a
dependency in your pubspec.yaml file.
Improvements:
- Internationalized default error texts for inbuilt validators - Help wanted to do even more in translating to more languages.
- Ability to programmatically induce an error to a field - could be especially useful for server-side validation.
- New field types including: SearchableDropdown and FilePickerField
- Better composition of validators.
Breaking changes:
- Rename
attribute
option in all fields toname
. validators
attribute has been renamed tovalidator
which takes Flutter's FormFieldValidator object. To compose multipleFormFieldValidator
s together, useFormBuilderValidators.compose()
which takes a list ofFormFieldValidator
objects.FormBuilderValidators.requiredTrue
functionality has been replaced withFormBuilderValidators.equal
which can be used to check equality of anyObject
or value- Due to its limited use,
FormBuilderCountryPicker
was removed from the package. Its functionality could be achieved with use ofFormBuilderSearchableDropdown
which is more extensible. FormBuilderCustomField
functionality is now achieved usingFormBuilderField
class which is the base class from which all fields are built in v4. Follow these instructions to construct your own custom form field usingFormBuilderField
.
final _formKey = GlobalKey<FormBuilderState>();
@override
Widget build(BuildContext context) {
return Column(
children: <Widget>[
FormBuilder(
key: _formKey,
autovalidate: true,
child: Column(
children: <Widget>[
FormBuilderFilterChip(
name: 'filter_chip',
decoration: InputDecoration(
labelText: 'Select many options',
),
options: [
FormBuilderFieldOption(
value: 'Test', child: Text('Test')),
FormBuilderFieldOption(
value: 'Test 1', child: Text('Test 1')),
FormBuilderFieldOption(
value: 'Test 2', child: Text('Test 2')),
FormBuilderFieldOption(
value: 'Test 3', child: Text('Test 3')),
FormBuilderFieldOption(
value: 'Test 4', child: Text('Test 4')),
],
),
FormBuilderChoiceChip(
name: 'choice_chip',
decoration: InputDecoration(
labelText: 'Select an option',
),
options: [
FormBuilderFieldOption(
value: 'Test', child: Text('Test')),
FormBuilderFieldOption(
value: 'Test 1', child: Text('Test 1')),
FormBuilderFieldOption(
value: 'Test 2', child: Text('Test 2')),
FormBuilderFieldOption(
value: 'Test 3', child: Text('Test 3')),
FormBuilderFieldOption(
value: 'Test 4', child: Text('Test 4')),
],
),
FormBuilderColorPickerField(
name: 'color_picker',
// initialValue: Colors.yellow,
colorPickerType: ColorPickerType.MaterialPicker,
decoration: InputDecoration(labelText: 'Pick Color'),
),
FormBuilderChipsInput(
decoration: InputDecoration(labelText: 'Chips'),
name: 'chips_test',
onChanged: _onChanged,
initialValue: [
Contact('Andrew', 'stock@man.com',
'https://d2gg9evh47fn9z.cloudfront.net/800px_COLOURBOX4057996.jpg'),
],
maxChips: 5,
findSuggestions: (String query) {
if (query.isNotEmpty) {
var lowercaseQuery = query.toLowerCase();
return contacts.where((profile) {
return profile.name
.toLowerCase()
.contains(query.toLowerCase()) ||
profile.email
.toLowerCase()
.contains(query.toLowerCase());
}).toList(growable: false)
..sort((a, b) => a.name
.toLowerCase()
.indexOf(lowercaseQuery)
.compareTo(b.name
.toLowerCase()
.indexOf(lowercaseQuery)));
} else {
return const <Contact>[];
}
},
chipBuilder: (context, state, profile) {
return InputChip(
key: ObjectKey(profile),
label: Text(profile.name),
avatar: CircleAvatar(
backgroundImage: NetworkImage(profile.imageUrl),
),
onDeleted: () => state.deleteChip(profile),
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
);
},
suggestionBuilder: (context, state, profile) {
return ListTile(
key: ObjectKey(profile),
leading: CircleAvatar(
backgroundImage: NetworkImage(profile.imageUrl),
),
title: Text(profile.name),
subtitle: Text(profile.email),
onTap: () => state.selectSuggestion(profile),
);
},
),
FormBuilderDateTimePicker(
name: 'date',
// onChanged: _onChanged,
inputType: InputType.time,
decoration: InputDecoration(
labelText: 'Appointment Time',
),
initialTime: TimeOfDay(hour: 8, minute: 0),
// initialValue: DateTime.now(),
// enabled: true,
),
FormBuilderDateRangePicker(
name: 'date_range',
firstDate: DateTime(1970),
lastDate: DateTime(2030),
format: DateFormat('yyyy-MM-dd'),
onChanged: _onChanged,
decoration: InputDecoration(
labelText: 'Date Range',
helperText: 'Helper text',
hintText: 'Hint text',
),
),
FormBuilderSlider(
name: 'slider',
validator: FormBuilderValidators.compose([
FormBuilderValidators.min(context, 6),
]),
onChanged: _onChanged,
min: 0.0,
max: 10.0,
initialValue: 7.0,
divisions: 20,
activeColor: Colors.red,
inactiveColor: Colors.pink[100],
decoration: InputDecoration(
labelText: 'Number of things',
),
),
FormBuilderCheckbox(
name: 'accept_terms',
initialValue: false,
onChanged: _onChanged,
title: RichText(
text: TextSpan(
children: [
TextSpan(
text: 'I have read and agree to the ',
style: TextStyle(color: Colors.black),
),
TextSpan(
text: 'Terms and Conditions',
style: TextStyle(color: Colors.blue),
),
],
),
),
validator: FormBuilderValidators.equal(
context,
true,
errorText:
'You must accept terms and conditions to continue',
),
),
FormBuilderTextField(
name: 'age',
decoration: InputDecoration(
labelText:
'This value is passed along to the [Text.maxLines] attribute of the [Text] widget used to display the hint text.',
),
onChanged: _onChanged,
// valueTransformer: (text) => num.tryParse(text),
validator: FormBuilderValidators.compose([
FormBuilderValidators.required(context),
FormBuilderValidators.numeric(context),
FormBuilderValidators.max(context, 70),
]),
keyboardType: TextInputType.number,
),
FormBuilderDropdown(
name: 'gender',
decoration: InputDecoration(
labelText: 'Gender',
),
// initialValue: 'Male',
allowClear: true,
hint: Text('Select Gender'),
validator: FormBuilderValidators.compose(
[FormBuilderValidators.required(context)]),
items: genderOptions
.map((gender) => DropdownMenuItem(
value: gender,
child: Text('$gender'),
))
.toList(),
),
FormBuilderTypeAhead(
decoration: InputDecoration(
labelText: 'Country',
),
name: 'country',
onChanged: _onChanged,
itemBuilder: (context, country) {
return ListTile(
title: Text(country),
);
},
controller: TextEditingController(text: ''),
initialValue: 'Uganda',
suggestionsCallback: (query) {
if (query.isNotEmpty) {
var lowercaseQuery = query.toLowerCase();
return allCountries.where((country) {
return country.toLowerCase().contains(lowercaseQuery);
}).toList(growable: false)
..sort((a, b) => a
.toLowerCase()
.indexOf(lowercaseQuery)
.compareTo(
b.toLowerCase().indexOf(lowercaseQuery)));
} else {
return allCountries;
}
},
),
FormBuilderRadioList(
decoration:
InputDecoration(labelText: 'My chosen language'),
name: 'best_language',
onChanged: _onChanged,
validator: FormBuilderValidators.compose(
[FormBuilderValidators.required(context)]),
options: ['Dart', 'Kotlin', 'Java', 'Swift', 'Objective-C']
.map((lang) => FormBuilderFieldOption(
value: lang,
child: Text('$lang'),
))
.toList(growable: false),
),
FormBuilderTouchSpin(
decoration: InputDecoration(labelText: 'Stepper'),
name: 'stepper',
initialValue: 10,
step: 1,
iconSize: 48.0,
addIcon: Icon(Icons.arrow_right),
subtractIcon: Icon(Icons.arrow_left),
),
FormBuilderRating(
decoration: InputDecoration(labelText: 'Rate this form'),
name: 'rate',
iconSize: 32.0,
initialValue: 1.0,
max: 5.0,
onChanged: _onChanged,
),
],
),
),
Row(
children: <Widget>[
Expanded(
child: MaterialButton(
color: Theme.of(context).accentColor,
child: Text(
"Submit",
style: TextStyle(color: Colors.white),
),
onPressed: () {
_formKey.currentState.save();
if (_formKey.currentState.validate()) {
print(_formKey.currentState.value);
} else {
print("validation failed");
}
},
),
),
SizedBox(width: 20),
Expanded(
child: MaterialButton(
color: Theme.of(context).accentColor,
child: Text(
"Reset",
style: TextStyle(color: Colors.white),
),
onPressed: () {
_formKey.currentState.reset();
},
),
),
],
)
],
);
}
Just add the FormBuilderLocalizations.delegate
in the list of your app's localizationsDelegates
return MaterialApp(
supportedLocales: [
Locale('en'),
Locale('it'),
Locale('fr'),
Locale('es'),
],
localizationsDelegates: [
FormBuilderLocalizations.delegate,
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
],
The currently supported fields include:
FormBuilderCheckbox
- Single Checkbox fieldFormBuilderCheckboxList
- List of Checkboxes for multiple selectionFormBuilderChipsInput
- Takes a list ofChip
s as input and suggests more options on typingFormBuilderChoiceChip
- Creates a chip that acts like a radio button.FormBuilderColorPicker
- ForColor
input selectionFormBuilderDateRangePicker
- For selection of a range of datesFormBuilderDateTimePicker
- ForDate
,Time
andDateTime
inputFormBuilderDropdown
- Used to select one value from a list as a DropdownFormBuilderFilePicker
- Picks image(s) from user device storage.FormBuilderFilterChip
- Creates a chip that acts like a checkbox.FormBuilderImagePicker
- Picks image(s) from Gallery or Camera.FormBuilderPhoneField
- International phone number input.FormBuilderRadioGroup
- Used to select one value from a list of Radio WidgetsFormBuilderRangeSlider
- Used to select a range from a range of valuesFormBuilderRating
- For selection of a numerical value as a ratingFormBuilderSearchableDropdown
- Field for selecting value(s) from a searchable listFormBuilderSegmentedControl
- For selection of a value from theCupertinoSegmentedControl
as an inputFormBuilderSignaturePad
- Field with drawing pad on which user can doodleFormBuilderSlider
- For selection of a numerical value on a sliderFormBuilderSwitch
- On/Off switch fieldFormBuilderTextField
- A Material Design text field input.FormBuilderTouchSpin
- Selection of a number by tapping on a plus or minus iconFormBuilderTypeAhead
- Auto-completes user input from a list of items
In order to create an input field in the form, along with the label, and any applicable validation, there are several attributes that are supported by all types of inputs namely:
Attribute | Type | Default | Required | Description |
---|---|---|---|---|
name |
String |
Yes |
This will form the key in the form value Map | |
initialValue |
T |
null |
No |
The initial value of the input field |
enabled |
bool |
true |
No |
Determines whether the field widget will accept user input. |
decoration |
InputDecoration |
InputDecoration() |
No |
Defines the border, labels, icons, and styles used to decorate the field. |
validator |
FormFieldValidator<T> |
null |
No |
A FormFieldValidator that will check the validity of value in the FormField |
onChanged |
ValueChanged<T> |
null |
No |
This event function will fire immediately the the field value changes |
valueTransformer |
ValueTransformer<T> |
null |
No |
Function that transforms field value before saving to form value. e.g. transform TextField value for numeric field from String to num |
The rest of the attributes will be determined by the type of Widget being used. |
To build your own field within a FormBuilder
, we use FormBuilderField
which will require that you define your own field.
var options = ["Option 1", "Option 2", "Option 3"];
FormBuilderField(
name: "name",
validator: FormBuilderValidators.compose([
FormBuilderValidators.required(context),
]),
builder: (FormFieldState<dynamic> field) {
return InputDecorator(
decoration: InputDecoration(
labelText: "Select option",
contentPadding:
EdgeInsets.only(top: 10.0, bottom: 0.0),
border: InputBorder.none,
errorText: field.errorText,
),
child: Container(
height: 200,
child: CupertinoPicker(
itemExtent: 30,
children: options.map((c) => Text(c)).toList(),
onSelectedItemChanged: (index) {
field.didChange(options[index]);
},
),
),
);
},
),
You can either change the value of one field at a time like so:
_formKey.currentState.fields['color_picker'].didChange(Colors.black);
Or multiple fields value like so:
_formKey.currentState.patchValue({
'age': '50',
'slider': 6.7,
'filter_chip': ['Test 1'],
'choice_chip': 'Test 2',
'rate': 4,
'chips_test': [
Contact('Andrew', 'stock@man.com', 'https://d2gg9evh47fn9z.cloudfront.net/800px_COLOURBOX4057996.jpg'),
],
});
The validator
attribute in fields take in a FormFieldValidator
which checks the validity of the
field. A FormFieldValidator
returns null
if validation is successful and a String
for the
errorText
if validation fails.
This package comes with several most common FormFieldValidator
s such as required, numeric, mail,
URL, min, max, minLength, maxLength, IP, credit card etc. with default errorText
.
Available built-in validators include:
FormBuilderValidators.creditCard()
- requires the field's value to be a valid credit card number.FormBuilderValidators.date()
- requires the field's value to be a valid date string.FormBuilderValidators.email()
- requires the field's value to be a valid email address.FormBuilderValidators.equal()
- requires the field's value be equal to provided object.FormBuilderValidators.integer()
- requires the field's value to be an integer.FormBuilderValidators.ip()
- requires the field's value to be a valid IP address.FormBuilderValidators.match()
- requires the field's value to match the provided regex pattern.FormBuilderValidators.max()
- requires the field's value to be less than or equal to the provided number.FormBuilderValidators.maxLength()
- requires the length of the field's value to be less than or equal to the provided maximum length.FormBuilderValidators.min()
- requires the field's value to be greater than or equal to the provided number.FormBuilderValidators.minLength()
- requires the length of the field's value to be greater than or equal to the provided minimum length.FormBuilderValidators.numeric()
- requires the field's value to be a valid number.FormBuilderValidators.required()
- requires the field have a non-empty value.FormBuilderValidators.url()
- requires the field's value to be a valid url.
FormBuilderValidators
class comes with a very useful static function named compose()
which takes
any number of FormFieldValidator
functions. On validation each validator is run and if any returns
a non-null value (i.e. a String), validation fails and the errorText
for the field is set as the
returned string.
Validation example:
FormBuilderTextField(
name: 'age',
decoration: InputDecoration(labelText: 'Age'),
validator: FormBuilderValidators.compose([
FormBuilderValidators.numeric(context, errorText: 'La edad debe ser numérica.'),
FormBuilderValidators.max(context, 70),
(val){
if(val < 0)
return 'We cannot have a negative age';
return null;
}
]),
),
Declare a variable to hold your error:
String _emailError;
Use the variable as the errorText
within InputDecoration
FormBuilderTextField(
name: 'email',
decoration: InputDecoration(
labelText: 'Email',
errorText: _emailError,
),
validator: FormBuilderValidators.compose([
FormBuilderValidators.required(context),
FormBuilderValidators.email(context),
]),
),
Set the error text
RaisedButton(
child: Text('Submit'),
onPressed: () async {
setState(() => _emailError = null);
if(checkIfEmailExists()){
setState(() => _emailError = 'Email already taken.');
}
},
),
You can also validate a field based on the value of another field
FormBuilderRadioGroup(
decoration: InputDecoration(labelText: 'My best language'),
name: 'my_language',
validator: FormBuilderValidators.required(context),
options: [
'Dart',
'Kotlin',
'Java',
'Swift',
'Objective-C',
'Other'
]
.map((lang) => FormBuilderFieldOption(value: lang))
.toList(growable: false),
),
FormBuilderTextField(
name: 'specify',
decoration:
InputDecoration(labelText: 'If Other, please specify'),
validator: (val) {
if (_formKey.currentState.fields['my_language']?.value ==
'Other' &&
(val == null || val.isEmpty)) {
return 'Kindly specify your language';
}
return null;
},
),
Any kind of support in the form of reporting bugs, answering questions or PRs is always appreciated.
We especially welcome efforts to internationalize/localize the package by translating the default
validation errorText
strings.
-
With the app’s root directory as the current directory, generate
l10n/intl_messages.arb
fromlib/localization/form_builder_localizations.dart
:flutter pub pub run intl_translation:extract_to_arb --output-dir=lib/l10n lib/localization/form_builder_localizations.dart
-
The
intl_messages.arb
file is a JSON format map with one entry for eachIntl.message()
function defined inlib/localization/form_builder_localizations.dart
. This file serves as a template for the different translations (for exampleintl_en.arb
andintl_es.arb
are English and Spanish translations respectively). You are therefore you are required to copy theintl_messages.arb
and put the content in a new file with the name of your locale with a name with formatintl_<locale>.arb
(e.g.intl_fr.arb
for French Translations). -
Translate the messages in the new file to the required language.
-
With the app’s root directory as the current directory, generate
intl_messages_<locale>.dart
for yourintl_<locale>.arb
file and updateintl_messages_all.dart
, which imports all of the messages files:
flutter pub run intl_translation:generate_from_arb --output-dir=lib/l10n --no-use-deferred-loading lib/localization/form_builder_localizations.dart lib/l10n/intl_<en>.arb lib/l10n/intl_messages.arb
e.g. To generate for French run: flutter pub run intl_translation:generate_from_arb --output-dir=lib/l10n --no-use-deferred-loading lib/localization/form_builder_localizations.dart lib/l10n/intl_fr.arb lib/l10n/intl_messages.arb
- Alternatively you could run the following command to generate Dart translation files for all the
intl_<locale>.arb
files in thel10n/
directory:
flutter pub pub run intl_translation:generate_from_arb --output-dir=lib/l10n --no-use-deferred-loading lib/localization/form_builder_localizations.dart lib/l10n/intl_*.arb
- Include your new language to
FormBuilderLocalization
's supported languages. Go tolib/localization/form_builder_localizations.dart
and include the language like so:
@override
bool isSupported(Locale locale) {
return ['en', 'es', 'fr'].contains(locale.languageCode);
}
- Submit your PR and be of help to millions of people all over the world!
If this package was helpful to you in delivering your project or you just wanna to support this package, a cup of coffee would be highly appreciated ;-)
Made with contributors-img.