How to create dynamically generated and validate TextFields in Flutter

How to create dynamically generated and validate TextFields in Flutter

In this post, we will look at how to create a dynamic generating and validated form with TextFormFields in Flutter.

Yesterday I saw a post on Twitter from a friend on an issue he was facing. He wanted to "how to create multiple textfield using listview.builder in flutter and get their values". The tweet can be found here.

link to tweet: https://twitter.com/Freaking_Colin/status/1596255415473422336?s=20&t=fUnPy9EptiDd4z9XHONfbg

And since I had done something similar in the past, I decided to share my knowledge with this blog post for future reference if need be.

We will start by creating a new flutter project

flutter create my_form

Then open the project with your favourite text editor/IDE. In my case, I will use vscode.

Screenshot 2022-11-28 at 11.05.13 PM.png

Let's head into the lib folder and start coding. First of all, I will clean up the project by removing the comments and default code. So we have this:

Screenshot 2022-11-28 at 11.07.52 PM.png

We will create a stateful widget called MultiFormWidget and then create a typedef function called OnDelete and use it as a parameter in the MultiFormWidget class. Add a boolean function isValid() is just under the createState method We will have,

typedef OnDelete = Function();

/// MultiFormWidget
class MultiFormWidget extends StatefulWidget {
  const MultiFormWidget({super.key, this.onDelete});

  final OnDelete? onDelete;

  @override
  State<MultiFormWidget> createState() => _MultiFormWidgetState();

 bool isValid() => _MultiFormWidgetState().validate();
}

class _MultiFormWidgetState extends State<MultiFormWidget> {
  @override
  Widget build(BuildContext context) {
    return Container();
  }
}

Now things are going to move fast! Let's create our data model class Student

class Student {
 String name;
 int age;

  Student(this.name, this.age);
}

And use it as a parameter in the MultiFormWidget class. We will go ahead and create our form. It will look like this

typedef OnDelete = Function();

/// MultiFormWidget
class MultiFormWidget extends StatefulWidget {
  const MultiFormWidget({super.key, this.onDelete, this.student});

  final OnDelete? onDelete;
  final Student? student;

  @override
  State<MultiFormWidget> createState() => _MultiFormWidgetState();

  bool isValid() => _MultiFormWidgetState().validate();
}

class _MultiFormWidgetState extends State<MultiFormWidget> {
  final _nameController = TextEditingController();
  final _ageController = TextEditingController();
  final form = GlobalKey<FormState>();
  @override
  Widget build(BuildContext context) {
    return Container(
   margin: const EdgeInsets.symmetric(horizontal: 8, vertical: 8),
      width: MediaQuery.of(context).size.width,
      height: 180,
      child: Scaffold(
      appBar: AppBar(
        leading: const Icon(Icons.verified_user, color: Colors.white),
        elevation: 0,
        title: const Text('Student Form'),
        centerTitle: true,
        actions: <Widget>[
          IconButton(
            icon: const Icon(
              Icons.delete,
              color: Colors.white,
            ),
            onPressed: widget.onDelete,
          )
        ],
      ),
      body: Form(
          key: form,
          child: Column(
          children: [
          const SizedBox(height: 10),
          TextFormField(
            controller: _nameController,
            onSaved: (val) => widget.student!.name = val!,
            style: Theme.of(context).textTheme.bodyText1,
            textInputAction: TextInputAction.next,
            keyboardType: TextInputType.name,
            maxLines: 1,
            enableInteractiveSelection: true,
            decoration: InputDecoration(
              labelText: 'Name',
              alignLabelWithHint: true,
              hintStyle: Theme.of(context).inputDecorationTheme.hintStyle,
              contentPadding: const EdgeInsets.all(10.0),
            ),
            validator: (value) {
              if (value!.isEmpty) {
                return 'Name cannot be Empty.';
              }
              return null;
            },
          ),
          const SizedBox(height: 10),
          TextFormField(
            controller: _ageController,
            onSaved: (val) => widget.student!.age = int.parse(val!),
            style: Theme.of(context).textTheme.bodyText1,
            textInputAction: TextInputAction.done,
            keyboardType: TextInputType.number,
            maxLines: 1,
            enableInteractiveSelection: true,
            decoration: InputDecoration(
              labelText: 'Age',
              alignLabelWithHint: true,
              hintStyle: Theme.of(context).inputDecorationTheme.hintStyle,
              contentPadding: const EdgeInsets.all(10.0),
            ),
            validator: (value) {
              if (value!.isEmpty) {
                return 'Age cannot be Empty.';
              }
              return null;
            },
          ),
        ],
      )),
    ));
  }

  bool validate() {
    var valid = form.currentState!.validate();
    if (valid) form.currentState!.save();
    return valid;
  }
}

Now add a List of MultiFormWidget in the HomeView class. Import collections

import 'package:collection/collection.dart';

then check if the List of MultiFormWidget is empty and handle that. Our code will look like this:

class HomeView extends StatefulWidget {
  const HomeView({super.key});

  @override
  State<HomeView> createState() => _HomeViewState();
}

class _HomeViewState extends State<HomeView> {
  List<MultiFormWidget> pages = []; // <---- add this

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(),
       body: Container(
        child: pages.isEmpty
            ? Center(
                child: Column(
                  crossAxisAlignment: CrossAxisAlignment.center,
                  mainAxisAlignment: MainAxisAlignment.center,
                  mainAxisSize: MainAxisSize.max,
                  children: [
                    const Text('Tap on button to add'),
                    TextButton.icon(
                        onPressed: onAddForm,
                        icon: const Icon(Icons.add),
                        label: const Text('Add new')),
                  ],
                ),
              )
            : ListView.separated(
                padding: const EdgeInsets.all(24),
                addAutomaticKeepAlives: true,
                shrinkWrap: true,
                separatorBuilder: (_, index) => const Divider(
                      color: Colors.white,
                    ),
                itemCount: pages.isEmpty ? 0 : pages.length,
                itemBuilder: (_, index) => pages[index]),
      ),
      floatingActionButton: Visibility(
        visible: pages.isNotEmpty,
        child: FloatingActionButton(
          onPressed: onAddForm,
          child: const Icon(Icons.add),
        ),
      ),
    );
  }

  ///on form user deleted
  void onDelete(Student student) {
    setState(() {
      final find = pages.firstWhereOrNull(
        (it) => it.student == student,
      );
      if (find != null) pages.removeAt(pages.indexOf(find));
    });
  }

  ///on add form
  void onAddForm() {
    setState(() {
      final student = Student();
      pages.add(MultiFormWidget(
        student: student,
        onDelete: () => onDelete(student),
      ));
    });
  }
}

Now let's make some further changes to our AppBar by adding a title and an action button

      appBar: AppBar(
        title: const Text(
          'Add Students',
        ),
        actions: <Widget>[
          TextButton(
            style: TextButton.styleFrom(
                textStyle: const TextStyle(
              color: Colors.white,
            )),
            onPressed: onSaveForm,
            child: Text(
              'Submit',
              style: Theme.of(context)
                  .textTheme
                  .bodyText2!
                  .copyWith(color: Colors.white, fontSize: 18),
            ),
          )
        ],
      ),

Let's write the onSaveForm function

  /// on save form
  void onSaveForm() {
    if (pages.isNotEmpty) {
      bool allValid = true;
      for (final form in pages) {
        // loop and check if all input fields a validated
        allValid = allValid && form.isValid();
      }
      if (allValid) {
        // all input fields are valid, get values
        final data = pages.map((it) => it.student).toList();

        /// you can go ahead and further manipulate the data or make a network call to your API
        print('data: $data');
      }
    }
  }

You should see something like this when you run and tap the add button

Well done👍🏾, you’ve made it to the end🥳. You can locate the source code here. If you have any questions, kindly leave a comment or connect with me on Twitter (Etornam Sunu)