Dynamic TextFormFields & Validations for Itineraries| A Flutter guide

Mustafa Tahir
6 min readAug 3, 2022

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

UI

- 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

Separate Widget

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(),
),
),
);
});
}
Response

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.

--

--