Working with Dynamic Multi-Form in Flutter With Validation

Yatin Deokar
7 min readMar 29, 2021

--

Sometimes we need to add FormField dynamically or provide multiple dynamic forms to the user for multiple data entries such as add multiple contacts. In this case, we have to manage its form field controllers and apply validation to each dynamic field. So let’s see how we can achieve this in a simple way.

What we cover in this article:

  • Add multiple forms dynamically
  • Remove Form dynamically
  • Apply Validation to each Form Field.
  • Clear Each Form Field Data
  • Validate All forms and Get Each form Data on Submit

Let’s start with actual implementation:

To Start with Flutter visit the Flutter official installation guide

1. Create a Data Model

Here we create ContactModel which contains contact name, contact number, and contact email. Which are basically our form fields. We will use this model to store form data.

Code:

class ContactModel {
String name;
String number;
String email;

ContactModel({this.name, this.number, this.email, this.address});
}

3. Create Form Widget

Here we create a single contact form widget with Form Fields like Name, Number, and Email as follows. and create Clear button to clear form and Remove/Delete button to delete specific form

Form Widget should be StatefulWidget so each form will maintain its own state
Note: Declare all your TextEditingController’s outside the state class
Don't forget to Add GlobalKey to the form to identify each form separately and validate the form by geting form state

Code:

class ContactFormItemWidget extends StatefulWidget {
ContactFormItemWidget(
{Key key, this.contactModel, this.onRemove, this.index})
: super(key: key);

final index;
ContactModel contactModel;
final Function onRemove;
final state = _ContactFormItemWidgetState();

@override
State<StatefulWidget> createState() {
return state;
}

TextEditingController _nameController = TextEditingController();
TextEditingController _contactController = TextEditingController();
TextEditingController _emailController = TextEditingController();

}

class _ContactFormItemWidgetState extends State<ContactFormItemWidget> {
final formKey = GlobalKey<FormState>();

@override
Widget build(BuildContext context) {
return Material(
child: Padding(
padding: const EdgeInsets.all(12.0),
child: Form(
key: formKey,
child: Container(
padding: const EdgeInsets.only(left: 12, right: 12, bottom: 12),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.all(Radius.circular(12)),
boxShadow: [
BoxShadow(
color: Colors.grey.withOpacity(0.2),
spreadRadius: 2,
blurRadius: 10,
offset: Offset(0, 3), // changes position of shadow
),
],
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Row(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
"Contact - ${widget.index}",
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 16,
color: Colors.orange),
),
Row(
mainAxisSize: MainAxisSize.min,
children: [
TextButton(
onPressed: () {
setState(() {
//Clear All forms Data
widget.contactModel.name = "";
widget.contactModel.number = "";
widget.contactModel.email = "";
widget._nameController.clear();
widget._contactController.clear();
widget._emailController.clear();
});
},
child: Text(
"Clear",
style: TextStyle(color: Colors.blue),
)),
TextButton(
onPressed: () => widget.onRemove(),
child: Text(
"Remove",
style: TextStyle(color: Colors.blue),
)),
],
),
],
),
TextFormField(
controller: widget._nameController,
// initialValue: widget.contactModel.name,
onChanged: (value) => widget.contactModel.name = value,
onSaved: (value) => widget.contactModel.name = value,
decoration: InputDecoration(
contentPadding: EdgeInsets.symmetric(horizontal: 12),
border: OutlineInputBorder(),
hintText: "Enter Name",
labelText: "Name",
),
),
SizedBox(
height: 8,
),
TextFormField(
controller: widget._contactController,
onChanged: (value) => widget.contactModel.number = value,
onSaved: (value) => widget.contactModel.name = value,
decoration: InputDecoration(
contentPadding: EdgeInsets.symmetric(horizontal: 12),
border: OutlineInputBorder(),
hintText: "Enter Number",
labelText: "Number",
),
),
SizedBox(
height: 8,
),
TextFormField(
controller: widget._emailController,
onChanged: (value) => widget.contactModel.email = value,
onSaved: (value) => widget.contactModel.email = value,
decoration: InputDecoration(
contentPadding: EdgeInsets.symmetric(horizontal: 12),
border: OutlineInputBorder(),
hintText: "Enter Email",
labelText: "Email",
),
),
],
),
),
),
),
);
}


}

Result:

Here we create StatefulWidget `ContactFormItemWidget` which is single contact form. Pass this widget a contact model.

5. Add multiple Forms Dynamically

  • Create another StatefulWidget to display Forms List.
  • Create a List that holds Contact Form Widget. Initially, It will be an empty list when the user clicks on Add We will add FormWidget to the list.
List<ContactFormItemWidget> contactForms = List.empty(growable: true);
  • Create ListView which renders Contact Form Items:
ListView.builder(
itemCount: contactForms.length,
itemBuilder: (_, index) {
return contactForms[index];
})
  • Create Add form and submit form button. Checkout Full Class File.

Initially, the form list is empty. so add an empty view or message

Add Form in FormList:

  • Add contact form Widget in contact forms list and pass ContactModel with Form index. you will get a remove/Delete call back here only.

Code:

//Add New Form
onAdd() {
setState(() {
ContactModel _contactModel = ContactModel(id: contactForms.length);
contactForms.add(ContactFormItemWidget(
index: contactForms.length,
contactModel: _contactModel,
onRemove: () => onRemove(_contactModel),
));
});
}

6. Delete Single Form

  • To delete any form we just need to remove Form from the FormWidgetList by identifying the index of a particular form:

Code:

//Delete/Remove specific form 
onRemove(ContactModel contact) {
setState(() {
int index = contactForms
.indexWhere((element) => element.contactModel.id == contact.id);

if (contactForms != null) contactForms.removeAt(index);
});
}

Result:

7. Clear Form Fields

  • Clear all controllers and ContactModel data onClick of clear button inside `ContactFormItemWidget`

Code:

TextButton(
onPressed: () {
setState(() {
//Clear All forms Data
widget.contactModel.name = "";
widget.contactModel.number = "";
widget.contactModel.email = "";
widget._nameController.clear();
widget._contactController.clear();
widget._emailController.clear();
});
},
child: Text(
"Clear",
style: TextStyle(color: Colors.blue),
)),

8. Define Form Validation and Check for All Forms

  • It seems difficult to manage dynamic forms validation of each form field, Let's see how we can do it easily.
  • First, we need to apply validation on each required form field. you can add more validation here eg. @ is required for email or the number length should be 10 digits etc.
  • If validation is passed it should return null otherwise error message.

Code:

TextFormField(
controller: widget._nameController,
onChanged: (value) => widget.contactModel.name = value,
onSaved: (value) => widget.contactModel.name = value,
//Add Validation for required field
validator: (value) => value.length > 3 ? null : "Enter Name",
decoration: InputDecoration(
contentPadding: EdgeInsets.symmetric(horizontal: 12),
border: OutlineInputBorder(),
hintText: "Enter Name",
labelText: "Name",
),
),
  • Then create a validation function that runs Form validation that's it validation is done. it's for the particular form. but you can check the validation for all the forms field by using formKey like below.

Code:

bool validate() { 
//Validate Form Fields by form key
bool validate = formKey.currentState.validate();
if (validate) formKey.currentState.save();
return validate;
}
  • We need to Access Form Validation function outside the ContactFormItemWidget, so create isValidated function in `ContactFormItemWidget` and assign validate() to it. It will return true or false as form validated or not.

Code:

bool isValidated() => state.validate();

9. Get All Forms Data and Submit

All the form data is stored in its own model class. so we just need to access the model for each form.

Code:

onSave() {
bool allValid = true;

//If any form validation function returns false means all forms are not valid
contactForms
.forEach((element) => allValid = (allValid && element.isValidated()));

if (allValid) {
for (int i = 0; i < contactForms.length; i++) {
ContactFormItemWidget item = contactForms[i];
debugPrint("Name: ${item.contactModel.name}");
debugPrint("Number: ${item.contactModel.number}");
debugPrint("Email: ${item.contactModel.email}");
}
//Submit Form Here
} else {
debugPrint("Form is Not Valid");
}
}

Full Class Code: MultiContactFormWidget

class MultiContactFormWidget extends StatefulWidget {
@override
State<StatefulWidget> createState() {
return _MultiContactFormWidgetState();
}
}

class _MultiContactFormWidgetState extends State<MultiContactFormWidget> {
List<ContactFormItemWidget> contactForms = List.empty(growable: true);

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Create Multi Contacts"),
),
bottomNavigationBar: Padding(
padding: const EdgeInsets.all(8.0),
child: CupertinoButton(
color: Theme.of(context).primaryColor,
onPressed: () {
onSave();
},
child: Text("Save"),
),
),
floatingActionButton: FloatingActionButton(
backgroundColor: Colors.orange,
child: Icon(Icons.add),
onPressed: () {
onAdd();
},
),
body: contactForms.isNotEmpty
? ListView.builder(
itemCount: contactForms.length,
itemBuilder: (_, index) {
return contactForms[index];
})
: Center(child: Text("Tap on + to Add Contact")),
);
}

//Validate all forms and submit
onSave() {
bool allValid = true;

//If any form validation function returns false means all forms are not valid
contactForms
.forEach((element) => allValid = (allValid && element.isValidated()));

if (allValid) {
for (int i = 0; i < contactForms.length; i++) {
ContactFormItemWidget item = contactForms[i];
debugPrint("Name: ${item.contactModel.name}");
debugPrint("Number: ${item.contactModel.number}");
debugPrint("Email: ${item.contactModel.email}");
}
//Submit Form Here
} else {
debugPrint("Form is Not Valid");
}
}

//Delete specific form
onRemove(ContactModel contact) {
setState(() {
int index = contactForms
.indexWhere((element) => element.contactModel.id == contact.id);

if (contactForms != null) contactForms.removeAt(index);
});
}

//Add New Form
onAdd() {
setState(() {
ContactModel _contactModel = ContactModel(id: contactForms.length);
contactForms.add(ContactFormItemWidget(
index: contactForms.length,
contactModel: _contactModel,
onRemove: () => onRemove(_contactModel),
));
});
}
}

Check out the Complete App Code on GIT Repository

Result:

That’s it for now, I hope it will help you to implement dynamic Forms in your next awesome Flutter project. Please provide feedback in the comments so, I can improve my tutorial and provide me a suggestion for any topic you need a tutorial.

Or you can connect with

Twitter: https://twitter.com/yatin_deokar

LinkedIn: https://www.linkedin.com/in/yatin-deokar-673a30106

GitHub: https://github.com/yatindeokar

--

--

Yatin Deokar
Yatin Deokar

Written by Yatin Deokar

Mobile Application Developer | Flutter Enthusiast| Android | Dart | Java | UI/UX Designer

Responses (1)