Better Flutter Forms with Reactive Forms

Better Flutter Forms with Reactive Forms

Writing forms in Flutter usually feels more like a chore than a fun task. Especially when the built-in solutions are less than convenient.

This is why we prepared this Flutter handbook with a couple of solutions for common problems. Following this guide, you can find easy steps for building simple forms with validation, styling existing text fields, focusing fields, and so on. 

The simple form

For this purpose, we will look at a simple form, which is the standard login form. Just an email and password will be enough for a start.

So, without further ado, to create a simple form you might want to follow these steps:

1. Create a widget with a Form

class SimpleForm extends StatefulWidget {
  const SimpleForm({Key? key}) : super(key: key);

  @override
  State<StatefulWidget> createState() => SimpleFormState();
}

class SimpleFormState extends State<SimpleForm> {
  TextEditingController emailController = new TextEditingController();
  TextEditingController passwordController = new TextEditingController();

  @override
  Widget build(BuildContext context) {
    return Form(
        child: Column(children: [

2. Add Fields to your form

For a simple form, a simple log-in input will be enough. So for this, we need to add 2 TextFormField widgets to our form. These widgets take up the role of storing and propagating their input values to the form. 

Additionally, our fields are going to be validated using the functions we describe in the validator function of the TextFormFields. In the case that the validator function finds that the input does not fit our criteria, the function may return an error message informing the user where they might have messed up. In the case we find no issue we must return null so that our form knows no issues exist with that particular input.

TextFormField(
  controller: emailController,
  decoration: InputDecoration(
    hintText: "Email",
  ),
  validator: (value) {
    if (value == null || value.isEmpty) {
      return 'Field cannot be empty';
    }
    if (!RegExp(r'^[a-zA-Z0-9-]+@[a-zA-Z0-9-]+.[a-zA-Z0-9-.]+$')
        .hasMatch(value)) {
      return 'Field must be email';
    }
    return null;
  },
),
TextFormField(
  controller: passwordController,
  decoration: InputDecoration(
    hintText: "Password",
  ),
  obscureText: true,
  validator: (value) {
    if (value == null || value.isEmpty) {
      return 'Field cannot be empty';
    }
    if (value.length < 8) {
      return 'Field must contain at least 8 characters';
    }
    return null;
  },
),

3. Submit the form

Lastly, we have to add a way to submit the form, in our case we want to log the user into our system. On a user press (when a user presses the submit button), we check if the form is valid and we try to log the user in.

ElevatedButton(
    onPressed: () {
      if (Form.of(context)!.validate()) {
        ScaffoldMessenger.of(context).showSnackBar(
            const SnackBar(content: const Text("Loggin in")));
      }
    },
    child: const Text("Log In"),
 )

4. Retrieve values

Now we have successfully created a form and the only thing missing is the data. In order to retrieve the data, we need to create field controllers which handle that data for us.

TextEditingController emailController = new TextEditingController();
TextEditingController passwordController = new TextEditingController();

Once created these controllers need to be added to the fields.

TextFormField(
        controller: emailController,

Now we have connected the field to the controller, which means all the functionally regarding the value of the field can now be accessed over the controller. 

This all seems fine, for this is a small example. On a larger scale, it will definitely not be an easy nor a pretty way to write a form. It becomes tedious writing additional code and the code itself can easily become messy. 

Reactive forms

For starters describing the data which the form will parse is done within something called a FormGroup. 

Instead of using The Form API integrated inside of Flutter, we opted to use a package based on a popular angular solution to the form problem which is called reactive forms. It offers a simple and elegant solution to the creation, expansion and maintenance of forms inside of flutter with a plethora of additional features such as specific fields often used in forms. Date, dropdown and so on. 

For starters describing the data which the form will parse is done within something called a FormGroup.

Take a look: 

FormGroup get form => fb.group(<Strig, dynamic>{
    'email': FormControl<String>(
      validators: [Validators.required, Validators.email],
    ),
    'assword': FormControl<String>(
      validators: [Validators.required, Validators.minLength(8)],
    ),
});

Inside the fields, we can describe their input values, initial values as well as define validation of the field with preexisting validators. Of course, this is not everything that it can offer, we can also describe validators on the entire form and even write our own validators.

We use these fields within a ReactiveFormBuilder which looks like this:

@override
Widget build(BuildContext context) {
  return ReactiveFormBuilder(
    form: () => form,
    builder: (context, form, child) {
      return Column(
        children: [
          ReactiveTextField(
            formControlName: 'email',
            validationMessages: (control) => {
              'required': "Email is required",
              'email': 'Field must be email',
            },
          ),
          ReactiveTextField(
            formControlName: 'password',
            validationMessages: (control) => {
              'required': "Password is required",
              'minLength': 'Field must contains at least 8 characters',
            },
          ),
          ReactiveFormConsumer(
            builder: (buildContext, form, child) {
              return TextButton(
                child: Text('Log in'),
                onPressed: () async {
                  if (form.valid) {
                    ScaffoldMessenger.of(context).showSnackBar(
                        const SnackBar(content: const Text("Loggin in")));
                    //log in
                  }
                },
              );
            },
          ),
        ],
      );
    },
  );
}

The form builder is assigned to the form which we described earlier. ReactiveForms provides us with basic form fields. In this example, we are using a simple ReactiveTextField but the package offers many more widely used fields. 

As we can see in the code the model and the UI are separated which makes the further expansion of the form or creation of new forms extremely easy. 

Conclusion

In conclusion, reactive forms will remove a lot of the boilerplate that you deal with on a regular basis whilst creating forms in Flutter. Which will in the end give you a better overview of the code and make your development time much shorter.

This is just a small snippet of what reactive forms are capable of, to find their true power we encourage you to explore the links above and create your next app with reactive forms.

You can view their tutorial here.

The solution we use is a package called reactive forms which is inspired by angulars reactive forms and gives a much more intuitive approach to creating forms in Flutter. You can check them out here .

Related Posts

Flutter Package for Cached HTTP Layer

Mislav Lukač — December 16, 2021

Flutter Package for Cached HTTP Layer

The first complete Flutter library for fetching, caching, and invalidating asynchronous data.

CoreLine Wins Clutch Award for Top Croatian Developer!

CoreLine — August 19, 2020

CoreLine Wins Clutch Award for Top Croatian Developer!

CoreLine is featured once again as one of the Clutch leaders in Croatia and the region.

See All Posts