TextFormFields have huge usage in Flutter Development, a lot of in all the other programming languages. How about using it dynamically? So, in this article, we’re going deep down into exploring Dynamic Formfields in Flutter, with real-world examples, code snippets, and much more as we learn. Let’s begin.
Table of Contents
1- A Real-world project structure
Consider a project having a “Plus icon” icon for storing the number of courses a student has passed. In addition, each course has different fields, and we won’t know the actual count of courses, so this is going to be dynamic. Right?
2- Implementing Dynamic Formfields
Head over to your project and create a new dart file dynamic_fields.dart
. Be sure to make it a Stateful Widget.
Note: For the sake of simplicity and to cover all levels, we shall keep it in the Stateful Widget to make use of it a little faster.
What the UI structure will be?
A FAB (Floating Action Button) for adding a new Formfield and incrementing the counter by 1
UI layout presenting Formfield and “Minus sign” to remove it by index
An AlertDialog to load up this record
- Here’s our first ready-to-use snippet
body: Padding(
padding: const EdgeInsets.all(8.0),
child: Align(
alignment: Alignment.topCenter,
child: CustomText(
text: "No Courses added",
fontSize: 30,
color: const Color(0xff000000),
),
),
)
Let’s run the app and get a start.
- The Logic Building stuff
We would require the following:
An <int> variable for storing the field count
A List<Widget> to add up the Formfields on each new tap
And more as we explore it
Declaring variables
int formFieldCount = 0;
List<Widget> widgetList = [];
FAB (Floating Action Button)
floatingActionButton: FloatingActionButton(
child: CustomText(text: "ADD NEW",),
onPressed: () {
setState(() {
formFieldCount++;
widgetList.add(buildFormField(formFieldCount));
});
},
),
And lastly, by modifying Scaffold’s body, we get,
body: formFieldCount == 0 ? Padding(
padding: const EdgeInsets.all(8.0),
child: Align(
alignment: Alignment.topCenter,
child: CustomText(
text: "No Courses added",
fontSize: 30,
color: const Color(0xff000000),
),
),
) : ListView.builder(
shrinkWrap: true,
padding: EdgeInsets.symmetric(horizontal: 5, vertical: 5),
itemCount: widgetList.length,
itemBuilder: (context, i) => Container()
),
As we mentioned in FAB’s onPressed
entity, there is a function buildField
with one argument. Let’s create that one.
This function would have the functionality to get us a new Formfield followed by its count.
- Here’s the code snippet
Widget buildFormField(int count) {
return ListTile(
leading: CircleAvatar(
child: CustomText(
text: count.toString(),
),
),
title: TextFormField(
decoration: InputDecoration(
border: const OutlineInputBorder(),
hintText: "Course ${count+1}"
),
),
trailing: const Icon(Icons.delete, color: Colors.red,),
);
}
At this point, reload the app and test it. We should receive our UI like this.
We have done half of the work. Now let’s add the following:
A List<Map> to store values in a key-value pair
Modify the Formfield’s onChanged entity and, from there, save the string
List<Map<String, dynamic>> data = [];
Followed by saveValue
function,
onChanged: (val) => saveValue(count + 1, val),
// Function
dynamic saveValue(int index, String value) {
data.add({
'course_id': index,
'course': value,
});
}
And lastly, we shall display this data inside a BottomSheet.
Note: We can also use AlertDialog, or Navigator (new screen), whichever suits you.
Under the same Listview, make a new ElevatedButton and jot down the code snippet for our BottomSheet. Furthermore, mapping the List<Map> values to ‘toList()‘ and retrieving the Courses.
- Code snippet
ElevatedButton(onPressed: () {
showModalBottomSheet(context: context, builder: (context) {
return ListView(
shrinkWrap: true,
children: data.map((e) => ListTile(
title: CustomText(
text: e['course'].toString().trim(),
color: const Color(0xff000000),
align: TextAlign.start,
),
)).toList(),
);
});
}, child: CustomText(text: "Show",),),
It’s time to test the app.
Since the onChanged
property expects each value as a new one, it stores the value in the function as a new index associated with a different value. So let’s fix this one.
Upon modifying the saveValue
function a bit, we get
dynamic saveValue(int index, String value) {
bool valueFound = false;
for (int j = 0; j < data.length; j++) {
if (data[j].containsKey("course_id")) {
if (data[j]["course_id"] == index) {
valueFound = !valueFound;
break;
}
}
}
/// If value is found
if (valueFound) {
data.removeWhere((e) => e["course_id"] == index);
}
data.add({
'course_id': index,
'course': value,
});
}
Don’t worry about the code, I will explain this in chunks.
We have added:
A bool to find out whether the value is already present or vice versa
A for loop to iterate over the items to match the index
Deletion of duplicate values
Here’s the UI view
Up till now, we have covered 90% of the work.
We are left with implementing:
- Formfield validations
- Removing them via index
Let’s get the second one done first, since it may not take more time.
Remove Formfield via index
Add an InkWell Widget to the existing trash icon and perform the following:
trailing: InkWell(child: const Icon(Icons.delete, color: Colors.red,), onTap: () {
setState(() {
widgetList.removeAt(count);
formFieldCount--;
/// Remove data from list<map>
data.removeAt(count);
});
},),
We’re done with this.
It’s time to add validation.
Formfield Validations
We shall be adding validation in the same way we do for a single Formfield. Such that:
Creating an instance of GlobalKey<FormState>
Wrapping our Widgets with Form & applying the key attribute
Adding validator(val) to our formfields
And lastly, a condition to test validation
// Instance
final GlobalKey<FormState> _formKey = GlobalKey<FormState>();
// Form field validator
validator: (val) => val!.isEmpty ? 'Required' : null,
ElevatedButton(onPressed: () {
if(_formKey.currentState!.validate()) {
debugPrint('done');
}
}, child: CustomText(text: "Validate",),
),
Our final Code Snippet says it all!
import 'package:flutter/material.dart';
import 'package:flutter_useful_tasks/widgets/custom_text.dart';
class DynamicFormFields extends StatefulWidget {
const DynamicFormFields({Key? key}) : super(key: key);
@override
_DynamicFormFieldsState createState() => _DynamicFormFieldsState();
}
class _DynamicFormFieldsState extends State<DynamicFormFields> {
int formFieldCount = 0;
List<Widget> widgetList = [];
List<Map<String, dynamic>> data = [];
final GlobalKey<FormState> _formKey = GlobalKey<FormState>();
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: CustomText(text: "Dynamic Fields",),
),
body: formFieldCount == 0 ? Padding(
padding: const EdgeInsets.all(8.0),
child: Align(
alignment: Alignment.topCenter,
child: CustomText(
text: "No Courses added",
fontSize: 30,
color: const Color(0xff000000),
),
),
) : Form(
key: _formKey,
child: Column(
children: [
ListView.builder(
shrinkWrap: true,
padding: EdgeInsets.symmetric(horizontal: 5, vertical: 5),
itemCount: widgetList.length,
itemBuilder: (context, i) => buildFormField(i)
),
ElevatedButton(onPressed: () {
showModalBottomSheet(context: context, builder: (context) {
return ListView(
shrinkWrap: true,
children: data.map((e) => ListTile(
title: CustomText(
text: e['course'].toString().trim(),
color: const Color(0xff000000),
align: TextAlign.start,
),
)).toList(),
);
});
}, child: CustomText(text: "Show",),),
ElevatedButton(onPressed: () {
if(_formKey.currentState!.validate()) {
debugPrint('done');
}
}, child: CustomText(text: "Validate",),),
],
),
),
floatingActionButton: FloatingActionButton(
child: CustomText(text: "ADD NEW",),
onPressed: () {
setState(() {
formFieldCount++;
widgetList.add(buildFormField(formFieldCount));
});
},
),
);
}
Widget buildFormField(int count) {
return ListTile(
leading: CircleAvatar(
child: CustomText(
text: (count + 1).toString(),
),
),
title: TextFormField(
decoration: InputDecoration(
border: const OutlineInputBorder(),
hintText: "Course ${count+1}"
),
onChanged: (val) => saveValue(count + 1, val),
validator: (val) => val!.isEmpty ? 'Required' : null,
),
trailing: InkWell(child: const Icon(Icons.delete, color: Colors.red,), onTap: () {
setState(() {
widgetList.removeAt(count);
formFieldCount--;
/// Remove data from list<map>
data.removeAt(count);
});
},),
);
}
dynamic saveValue(int index, String value) {
bool valueFound = false;
for (int j = 0; j < data.length; j++) {
if (data[j].containsKey("course_id")) {
if (data[j]["course_id"] == index) {
valueFound = !valueFound;
break;
}
}
}
/// If value is found
if (valueFound) {
data.removeWhere((e) => e["course_id"] == index);
}
data.add({
'course_id': index,
'course': value,
});
}
}
Run the app and you’re good at going.
Here’s the UI version
All done!
Hope you enjoyed reading this one.
Wrapping Up
In this enlightening article, we incorporated dynamic fields, implementing validation through Forms, and elegantly displaying strings on the BottomSheet. Through insightful code snippets and intuitive UI demonstrations, we explored the seamless integration of these features into our Flutter project.
For a hands-on experience and deeper understanding, you can access the full code repository here.
If you think I missed out on something or want to add your point of view, please jot it down in the comments section below. I would love to know that as well.
Additionally, stay connected with me on my YouTube channel, where I’m dedicated to sharing valuable insights and tutorials to empower Flutter developers worldwide.
Thanks for your precious time.