Dynamic Formfields in Flutter. An overview

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.

Initial UI
  • 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.

Form field UI

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.

Undesired output

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

Desired output

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

Validation

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.

– Other Blog posts —

Thanks for your precious time.

Leave a Comment