Working with Dynamic Multi-Form in Flutter With Validation

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:

1. Create a Data Model

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

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

3. Create Form Widget

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",
),
),
],
),
),
),
),
);
}


}

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.
  • Add contact form Widget in contact forms list and pass ContactModel with Form index. you will get a remove/Delete call back here only.
//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:
//Delete/Remove specific form 
onRemove(ContactModel contact) {
setState(() {
int index = contactForms
.indexWhere((element) => element.contactModel.id == contact.id);

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

7. Clear Form Fields

  • Clear all controllers and ContactModel data onClick of clear button inside `ContactFormItemWidget`
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.
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.
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.
bool isValidated() => state.validate();

9. Get All Forms Data 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");
}
}
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),
));
});
}
}

--

--

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store
Yatin Deokar

Yatin Deokar

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