Dynamic TextFormFields & Validations for Itineraries| A Flutter guide
You may be a good player in adding TextFormFields and styling them according to your wish but what if you come across a dynamic addition of FormFields in your project? As I mentioned, “Itineraries” above since I came across the same situation in one of my Flutter projects. So here’s a complete illustration of achieving this milestone.
Let’s get started!
Table of Contents
Project Structure
Logic Building & Implementation
Validations
- Project Structure
We will try to keep it simple.
We will be having the following:
Floating Action Button (FAB) to add a new Field
Dialog to display the strings we fetched
and finally TextFormField, each having ➖ Icon
- Logic Building & Implementation
We would require the following:
A counter variable of <int> for adding & subtracting FormFields
A list of type <Widget> to add the complete Widget stuff
and more as we explore…
1- Declaring variables
List<Widget> list = [];
int fieldCount = 0;
2- Building up the Widgets
Floating Action Button
floatingActionButton: FloatingActionButton(
child: const Text("ADD\nNEW"),
onPressed: () {
setState(() {
fieldCount++;
list.add(buildField(fieldCount));
});
},
),
List<Widgets> section
fieldCount == 0
? const Padding(
padding: EdgeInsets.all(15.0),
child: Align(
alignment: Alignment.center,
child: Text(
"No Itineraries added!",
style:
TextStyle(fontSize: 33, fontWeight: FontWeight.bold),
),
),
)
: ListView.builder(
itemCount: list.length,
shrinkWrap: true,
itemBuilder: (_, i) {},
),
Now, create a separate widget as “buildField” and pass the index we received from Listview.
Widget buildField(int i) {
return ListTile(
leading: CircleAvatar(
child: Text((i+1).toString()),
),
title: TextFormField(
decoration: InputDecoration(
border: const OutlineInputBorder(
borderRadius: BorderRadius.all(Radius.circular(8))
),
labelText: "Itinerary ${i+1}",
),
),
trailing: const Icon(Icons.delete_outlined,color: Colors.red),
);
}
Here’s the UI
Here, we’re done with half of the stuff.
How we can retrieve value from our dynamic FormFields?
Required
We will be having a List <Map<String, dynamic>> to store the string we receive. Secondly, we need to make use of onChanged property of TextField to get the latest string & finally we display data on the AlertDialog.
Create another function that stores the value as “storeValue” with parameters
Index (from buildField)
String (from onChanged)
Upon some modifications, we get
Declaring the <Map<String, dynamic>>
List<Map<String, dynamic>> items = [];
Updated function with onChanged
Widget buildField(int i) {
return ListTile(
leading: CircleAvatar(
child: Text((i+1).toString()),
),
title: TextFormField(
decoration: InputDecoration(
border: const OutlineInputBorder(
borderRadius: BorderRadius.all(Radius.circular(8))
),
labelText: "Itinerary ${i+1}",
),
onChanged: (data) => storeValue(i+1, data),
),
trailing: const Icon(Icons.delete_outlined,color: Colors.red),
);
}
function- storedValue(index, string)
dynamic storeValue(int i, String v) {
items.add({
"field_id": i,
"itinerary": v,
});
}
Here, we’re finalized with the stuff but, there is a small bug in this section. We cover it when surpassed the AlertDialog section.
Display stored strings on the AlertDialog
itinerariesDialog(BuildContext context) {
showDialog(
barrierDismissible: true,
context: context,
builder: (context) {
return AlertDialog(
title: Text("Stored Itineraries"),
content: Container(
width: double.maxFinite,
child: ListView(
shrinkWrap: true,
children:
items.map((e) =>
Text(e["itinerary"].trim())).toList(),
),
),
);
});
}
You can see how the data is being saved. Since we used the onChanged property so each time we add a new field, the list acts as adding a new string with a different ID.
Let’s fix this one.
We need to add some conditions for duplication etc.
Here’s the updated snippet
dynamic storeValue(int i, String v) {
bool valueFound = false;
for (int j = 0; j < items.length; j++) {
if (items[j].containsKey("field_id")) {
if (items[j]["field_id"] == i) {
valueFound = !valueFound;
break;
}
}
}
/// If value is found
if (valueFound) {
items.removeWhere((e) => e["field_id"] == i);
}
items.add({
"field_id": i,
"itinerary": v,
});
}
In the snippet above, I have created
a bool that checks for the value we receive
A for loop for iterating over the List
Removing items if found
On running the app again.
Congrats! We have successfully fixed this one.
However, we forgot to add the functionality once we press the “Trash” Icon & Validations. Let’s handle these ones too.
1- Functionality to remove TextFormField
Widget buildField(int i) {
return ListTile(
leading: CircleAvatar(
child: Text((i + 1).toString()),
),
title: TextFormField(
decoration: InputDecoration(
border: const OutlineInputBorder(
borderRadius: BorderRadius.all(Radius.circular(8))),
labelText: "Itinerary ${i + 1}",
),
onChanged: (data) => storeValue(i + 1, data),
),
trailing: InkWell(
child: const Icon(Icons.delete_outlined, color: Colors.red),
onTap: () {
setState(() {
fieldCount--;
list.removeAt(i); ///Remove from List<Map>> as well
items.removeAt(i);
});
},
),
);
}
Here’s the complete working version.
2– Applying Validations
We have to apply validations the way we do as normal developments.
At first, we need to
Create GlobalKey<FormState> instance
Adding validators
and we’re done
Our final Code Snippet says it all!
import 'package:flutter/material.dart';
class DynamicFields extends StatefulWidget {
const DynamicFields({Key? key}) : super(key: key);
@override
State<DynamicFields> createState() => _DynamicFieldsState();
}
class _DynamicFieldsState extends State<DynamicFields> {
List<Widget> list = [];
int fieldCount = 0;
List<Map<String, dynamic>> items = [];
final _formKey = GlobalKey<FormState>();
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text("Dynamic TextFormFields"),
actions: [
InkWell(
child: const Padding(
padding: EdgeInsets.all(8.0),
child: Icon(Icons.data_thresholding_rounded),
),
onTap: () => itinerariesDialog(context),
),
],
),
body: Form(
key: _formKey,
child: ListView(
shrinkWrap: true,
children: [
fieldCount == 0
? const Padding(
padding: EdgeInsets.all(15.0),
child: Align(
alignment: Alignment.center,
child: Text(
"No Itineraries added!",
style:
TextStyle(fontSize: 33, fontWeight: FontWeight.bold),
),
),
)
: Column(
children: [
ListView.builder(
itemCount: list.length,
shrinkWrap: true,
itemBuilder: (_, i) => buildField(i),
),
const SizedBox(height: 12),
ElevatedButton(onPressed: () {
if(!_formKey.currentState!.validate()){
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text("Fields missing")));
}
}, child: const Text("Validate")),
],
),
],
),
),
floatingActionButton: FloatingActionButton(
child: const Text("ADD\nNEW"),
onPressed: () {
setState(() {
fieldCount++;
list.add(buildField(fieldCount));
});
},
),
);
}
itinerariesDialog(BuildContext context) {
showDialog(
barrierDismissible: true,
context: context,
builder: (context) {
return AlertDialog(
title: const Text("Stored Itineraries"),
content: SizedBox(
width: double.maxFinite,
child: ListView(
shrinkWrap: true,
children:
items.map((e) => Text(e["itinerary"].trim())).toList(),
),
),
);
});
}
Widget buildField(int i) {
return ListTile(
leading: CircleAvatar(
child: Text((i + 1).toString()),
),
title: TextFormField(
decoration: InputDecoration(
border: const OutlineInputBorder(
borderRadius: BorderRadius.all(Radius.circular(8))),
labelText: "Itinerary ${i + 1}",
),
onChanged: (data) => storeValue(i + 1, data),
validator: (val) => val!.isEmpty ? "Required" : null,
),
trailing: InkWell(
child: const Icon(Icons.delete_outlined, color: Colors.red),
onTap: () {
setState(() {
fieldCount--;
list.removeAt(i);
items.removeAt(i);
});
},
),
);
}
dynamic storeValue(int i, String v) {
bool valueFound = false;
for (int j = 0; j < items.length; j++) {
if (items[j].containsKey("field_id")) {
if (items[j]["field_id"] == i) {
valueFound = !valueFound;
break;
}
}
}
/// If value is found
if (valueFound) {
items.removeWhere((e) => e["field_id"] == i);
}
items.add({
"field_id": i,
"itinerary": v,
});
}
}
Output
Final Tips
The data we receive w.r.t the Maps, we can place this complete List<Map> with in the Firebase as well. Realtime or Cloud Firestore (your choice…).
Let me know If I missed something or feel free to suggest me topic for my next article. Thanks!
That’s all!
If you found this quick one quite helpful then 👏.
Feel free to ask your queries here or on Twitter. I will try my best to respond asap.
P.S. I do have my own YouTube Channel where I upload content related to Flutter Series and GitHub etc. If you find your type of stuff then LIKE, SHARE & SUBSCRIBE as it motivates me to create more for you! Thanks.