DEV Community

Cover image for Why use Reactive Forms in Flutter?
Joan Pablo
Joan Pablo

Posted on • Edited on

Why use Reactive Forms in Flutter?

Haven't you ever felt that getting data from a Form, validating a field, or even disabling the submit button is tedious and full of boilerplate code?

Fortunately, that's why Reactive Forms exists.

1. Ease and clean way of collecting data from your Forms.

No more onSave, onChanges, onEditionCompleted callbacks needed or TextEditionControllers to collect data from inputs

Reactive Forms get a clean separation between views and model and maintains data synchronization in a transparent two-way binding mechanism. Just declare your model and define your widgets. Then lay back and relax, data will flow smoothly.

// group and controls
final form = FormGroup({
  'name': FormControl<String>(value: 'John Doe'),
  'email': FormControl<String>(),
});
Enter fullscreen mode Exit fullscreen mode
// two-way bindings with widgets
@override
Widget build(BuildContext context) {
  return ReactiveForm(
    formGroup: this.form,
    child: Column(
      children: <Widget>[
        ReactiveTextField(
          formControlName: 'name',
        ),
        ReactiveTextField(
          formControlName: 'email',
        ),
      ],
    ),
  );
}
Enter fullscreen mode Exit fullscreen mode

2. Transparent Validations of inputs fields.

No more StatefulWidget defining a key for the Form widget, goodbye to Form.of(context) and Goodbye to:

// boilerplate code...
if (form.validate()) {
  form.save();
} else {
  setState(() {
    _autoValidate = true;
  });
}
Enter fullscreen mode Exit fullscreen mode

Reactive Forms Validations occurs transparently. Just declare your model and define the validators you need. It will also handle validity of the entire Form and will shows error messages when needed. All of that without you needing to write a single line of imperative code.

// controls and validators
final form = FormGroup({
  'email': FormControl<String>(validators: [
    Validators.required,
    Validators.email,
  ]),
});
Enter fullscreen mode Exit fullscreen mode
// customize validation messages
@override
Widget build(BuildContext context) {
  return ReactiveForm(
    formGroup: this.form,
    child: Column(
      children: <Widget>[
        ReactiveTextField(
          formControlName: 'email',
          validationMessages: (control) => {
            ValidationMessage.required: 'The email must not be empty',
            ValidationMessage.email: 'The email value must be a valid email'
          },
        ),
      ],
    ),
  );
}
Enter fullscreen mode Exit fullscreen mode

3. Enable/Disable buttons depending of Form validity.

Getting this behavior, even in such a great framework as Flutter, can sometimes be hard and full of boilerplate code.

In Reactive Forms is as simple as asking to the Form if it is valid or not:

@override
  Widget build(BuildContext context) {
    final form = ReactiveForm.of(context);
    return RaisedButton(
      child: Text('Submit'),
      // just ask to the form
      onPressed: form.valid ? _onPressed : null,
    );
  }
Enter fullscreen mode Exit fullscreen mode

4. Perfect integration with Provider plugin and other state management plugins.

The use of a state management library (MVC, MVVM or just BLoc) cleans your code and perfectly separates responsibilities between the UI and business logic.

// your bloc/controller/viewmodel/etc
class SignInViewModel {
  final form = FormGroup({
    'email': FormControl<String>(validators: [Validators.required, Validators.email]),
    'password': FormControl<String>(validators: [Validators.required, Validators.minLength(8)])
  });

  void signIn() {
    final credentials = this.form.value;
    // ... make some business logic
    // ...
  }

}
Enter fullscreen mode Exit fullscreen mode
// simple sign-in view
class SignInScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final viewModel = Provider.of<SignInViewModel>(context, listen: false);
    return ReactiveForm(
      formGroup: viewModel.form,
      child: Column(
        children: <Widget>[
          ReactiveTextField(
            formControlName: 'email',
          ),
          ReactiveTextField(
            formControlName: 'password',
            obscureText: true,
          ),
          ReactiveFormConsumer(
            builder: (context, form, child) {
              return RaisedButton(
                child: Text('Submit'),
                // if form valid, sign-in
                onPressed: form.valid ? viewModel.signIn : null,
              );
            },
          ),
        ],
      ),
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

5. Focus/Unfocus text fields.

To add or remove focus to a control use FormControl.focus() or FormControl.unfocus(). No more StatefulWidgets and FocusNode declarations:

// declare a form and controls
final form = FormGroup({
  'name': FormControl<String>(value: 'John Doe'),
});

// the name control
final name = form.control('name');

// UI text field get focus and the device keyboard pop up
name.focus();

// UI text field lose focus
name.unfocus();
Enter fullscreen mode Exit fullscreen mode

What is Reactive Forms?

  • Reactive Forms provides a model-driven approach to handling form inputs whose values change over time. It's heavily inspired in Angular Reactive Form.

  • It lets you focus on business logic and save you time from collect, validate and maintain synchronization data between models and widgets.

  • Remove boilerplate code and brings you the possibility to write clean code with minimal effort.

  • Integrates perfectly with common state management libraries like Provider, Bloc, and many others.

What is not Reactive Forms?

  • Reactive Forms is not a fancy widgets package. It's not a widget's library with new shapes, colors, or animations. It frees you from the responsibility of gathering and validating the data. And keeps the data in sync between your model and your widgets.

  • Reactive Forms does not replace native widgets that you commonly use in Flutter like TextFormField, DropdownButtonFormField or CheckboxListTile. It brings new two-way binding capabilities and much more features to those same widgets.

Reactive Forms is much more. This POST was just a preview of some basic features. Please visit Github Repo or plugin page in puv.dev for full documentation.

Top comments (21)

Collapse
 
thisisrinzin profile image
Rinzin Wangchuk • Edited

Thanks for the package. I use angular at work and flutter for my personal projects. I was thinking of making sth like form builder for flutter and found that there was already one🥳🥳🥳. Any way I can contribute to the package although I am not a seasoned developer yet?.

Collapse
 
joanpablo profile image
Joan Pablo

Hi Rinzin,

Thank you for your comments. You are welcome to contribute to the package. Your experience is not a problem, together we can improve the package as much as necessary.

Collapse
 
zacharias02 profile image
John Lester D. Necesito • Edited

Hello may I ask, how can I convert the date and time format after I select date and time?
like 12-hour format (12:01 AM) for time and YYYY-MM-D for date

Lastly, how can I add validation messages to a reactive textfield v7.0.0+

Collapse
 
joanpablo profile image
Joan Pablo • Edited

Hi John Lester,

You can add a ControlValueAccessor to the reactive text field with the format.
In the Wiki you have an example of how to do that.

To add a validation message you just supply the validationMessages callback method in ReactiveTextField and return a Map with the messages:

ReactiveTextField(
   formControlName: 'email',
   validationMessages: (control) => {
      ValidationMessage.required: 'The email must not be empty',
      ValidationMessage.email: 'The email value must be a valid email'
   },
),
Enter fullscreen mode Exit fullscreen mode
Collapse
 
ctechdev profile image
ctechdev

Could you provide a working code example for ReactiveDatePicker?
I am trying but I always get an error "Invalid date format".
My code:
....
var last = DateTime.now ();
var firstDate= DateTime.parse ('2021-01-16');
.....
ReactiveDatePicker (
formControlName: 'birthDate',
firstDate: firstDate,
lastDate: last,
builder: (context, picker, child) {
return IconButton (
onPressed: picker.showPicker,
icon: Icon (Icons.date_range));
}),

Collapse
 
joanpablo profile image
Joan Pablo

Hi @ctechdev ,

I have released new versions of the package (v10.2.0). Have you test it? Have you created an issue in the github repo?

Collapse
 
echarish profile image
Harish Kumar

Hi John

Your library is life saver :D
One question I wanted to ask is if I wanted to change the decoration around the text fields, how should I do that? like the color of fields and colors of field names that show up the field when a text field is in focus. I understand it is taking defaults from the theme of app but if I wanted to do specific decoration for the text fields how should it be done.

Collapse
 
ctechdev profile image
ctechdev

Hi, I would like to use multiple reactive forms. I created a tabbarview with 6 different tabs and each view has a different reactive form. At the top of the appbar I created a button to save the data of all the forms in an Entry object. How can I implement this function without global keys using a provider that collects the data entered in all the tabs? Thanks in advance.

Collapse
 
joanpablo profile image
Joan Pablo

Hi ctechdev,

A simple way to create a wizard is for example to wrap all the widget within a ReactiveForm and each TabBarView can contains a ReactiveForm to collect specific data.

Collapse
 
zacharias02 profile image
John Lester D. Necesito

Hi! Can I ask if it is possible to add a dynamic reactive text field to a form?

Collapse
 
joanpablo profile image
Joan Pablo

Hi Jhon,

Right now there is a method FormGroup.addAll() that receives a collection of FormControls and append them as children. But maybe is not what you're asking for.

So I will add that as an issue in the GitHub repo and I will bring the possibility to add a single control FormGroup.add() and that action will trigger an event of type FormControlCollection.collectionChanges.

I will release it in next version.

Thanks again.

Collapse
 
joanpablo profile image
Joan Pablo

Hi John Lester,

I've just released v6.0.0.

In this version like in the previous one, you can add controls to a group with the method FormGroup.addAll(). This method receives a Map of controls as arguments. In this version the disrty/pristine state of the group is updated and it also triggers the onCollectionChanges event.

Collapse
 
zacharias02 profile image
John Lester D. Necesito

Hi may I ask how did you put an initial value coming from firebase?

Collapse
 
joanpablo profile image
Joan Pablo

Hi John,

You can just set the value to a control as soon as you get it or you can reset the control to and initial value (release 4.0.0)

// `fb.group` is just a syntax sugar, you can simple use FormGroup if you want.
final form = fb.group({
    'name': 'some init value'
});

_executeFirebaseAsyncMethod().then((newValue) {
    // sets the control init value
    form.value = {'name': newValue};
});

or

final form = fb.group({
    'name': 'some init value'
});

_executeFirebaseAsyncMethod().then((newValue) {
    // resets the control to untouched and set init value
    form.reset(value: {'name': newValue});
});
Collapse
 
zacharias02 profile image
John Lester D. Necesito • Edited

One more thing it is possible to untouch a reactiveTextfield upon editing?

For example, I have an async validation that will check if an email is existing then, when I go to the editing profile screen it automatically validates the value of that field which I don't like. I want to validate it when the value of that field changes.

Lastly, is it possible to add an additional parameter to custom validator?

Thread Thread
 
joanpablo profile image
Joan Pablo • Edited

Hello John,

Async validators executes after every Sync validator executes without validation errors. That means that if you add for example Validators.required or Validators.email to your ReactiveTextField only when those validators marks the input as valid then the Async Validator excecutes.

But I think you are right about the issue, I will release soon a new version that let you ask inside your validator if the control is "dirty" or not, so you can check if the value has changed or not and request the server in the async validator. Mean while you can implement a workaround by asking inside your async validator if the value of the FormControl is different from the value of your model.

About the Second question, yes you can create a custom validator with additional params, there are two ways of doing this:
1-) Create a function/method that receives arguments and returns a ValidatorFunction. An example of this is the Validators.mustMatch. You can check the README.md.
2-) Extend Validator class.

Creating a function/method

// controlName and matchingControlName as additional arguments
Map<String, dynamic> _mustMatch(String controlName, String matchingControlName) {
  // returns a ValidatorFunction
  return (AbstractControl control) {
    final form = control as FormGroup;

    final formControl = form.control(controlName);
    final matchingFormControl = form.control(matchingControlName);

    if (formControl.value != matchingFormControl.value) {
      matchingFormControl.addError({'mustMatch': true});

      // force messages to show up as soon as possible
      matchingFormControl.touch(); 
    } else {
      matchingFormControl.setErrors({});
    }

    return null;
  };
}

Usage

final form = fb.group({
   'password': ['', Validators.required],
   'passwordConfirmation': ['', Validators.required],
}, [_mustMatch('password', 'passwordConfirmation')]);

Extend Validator class (this is the implementation of Validators.pattern)

/// Validator that requires the control's value to match a regex pattern.
class PatternValidator extends Validator {
  final Pattern pattern;

  /// Constructs an instance of [PatternValidator].
  ///
  /// The [pattern] argument must not be null.
  PatternValidator(this.pattern) : assert(pattern != null);

  @override
  Map<String, dynamic> validate(AbstractControl control) {
    RegExp regex = new RegExp(this.pattern);
    return (control.value == null ||
            control.value == '' ||
            regex.hasMatch(control.value))
        ? null
        : {
            ValidationMessage.pattern: {
              'requiredPattern': this.pattern.toString(),
              'actualValue': control.value,
            }
          };
  }
}

Usage

final form = fb.group({
   'email': ['', PatternValidator('some pattern').validate],
});

Validators.mustMatch and Validators.pattern are already included in Reactive Forms so you don't have to implement them, the above code are just examples to get the idea.

Thread Thread
 
zacharias02 profile image
John Lester D. Necesito • Edited

Thank you so much for your help! I will look forward to the next version of your package! ❤

Collapse
 
zacharias02 profile image
John Lester D. Necesito

That's great! Thank you so much! 💞

Collapse
 
sherryqaisrani profile image
Shaher Yar

Hi, Joan Pablo
Hope you are doing well. I have a question regard reactive forms fields. I have an api in the response of api i got different forms information how i can impliment with this using reactive forms fields?

Collapse
 
zacharias02 profile image
John Lester D. Necesito • Edited

Hi Joan,

May I ask how to dynamically add and remove validator in a specific formControl.

For example,

imgur.com/a/ERz7wGa

Collapse
 
joanpablo profile image
Joan Pablo

Hi John,

In the recently version 10.2.0 you can add validators dynamically.