2

enter image description here

below is my code. i have 6 fields. all of them are showing error all at once. even after i submit the form. once it gets cleared. it would show the validation message as field cannot be empty /Name is required. how do i validate each field at a time. i don't want all the fields to show validate message as in the below image.

import 'package:flutter/material.dart';

class FormScreen extends StatefulWidget {
  @override
  State<StatefulWidget> createState() {
    return FormScreenState();
  }
}

class FormScreenState extends State<FormScreen> {
  String _name;
  String _email;
  String _password;
  String _url;
  String _phoneNumber;
  String _calories;

  final GlobalKey<FormState> _formKey = GlobalKey<FormState>();

  Widget _buildName() {
    return TextFormField(
      decoration: InputDecoration(labelText: 'Name'),
      maxLength: 10,
      validator: (String value) {
        if (value.isEmpty) {
          return 'Name is Required';
        }

        return null;
      },
      onSaved: (String value) {
        _name = value;
      },
    );
  }

  Widget _buildEmail() {
    return TextFormField(
      decoration: InputDecoration(labelText: 'Email'),
      validator: (String value) {
        if (value.isEmpty) {
          return 'Email is Required';
        }

        if (!RegExp(
                r"[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?")
            .hasMatch(value)) {
          return 'Please enter a valid email Address';
        }

        return null;
      },
      onSaved: (String value) {
        _email = value;
      },
    );
  }

  Widget _buildPassword() {
    return TextFormField(
      decoration: InputDecoration(labelText: 'Password'),
      keyboardType: TextInputType.visiblePassword,
      validator: (String value) {
        if (value.isEmpty) {
          return 'Password is Required';
        }

        return null;
      },
      onSaved: (String value) {
        _password = value;
      },
    );
  }

  Widget _builURL() {
    return TextFormField(
      decoration: InputDecoration(labelText: 'Url'),
      keyboardType: TextInputType.url,
      validator: (String value) {
        if (value.isEmpty) {
          return 'URL is Required';
        }

        return null;
      },
      onSaved: (String value) {
        _url = value;
      },
    );
  }

  Widget _buildPhoneNumber() {
    return TextFormField(
      decoration: InputDecoration(labelText: 'Phone number'),
      keyboardType: TextInputType.phone,
      validator: (String value) {
        if (value.isEmpty) {
          return 'Phone number is Required';
        }

        return null;
      },
      onSaved: (String value) {
        _url = value;
      },
    );
  }

  Widget _buildCalories() {
    return TextFormField(
      decoration: InputDecoration(labelText: 'Calories'),
      keyboardType: TextInputType.number,
      validator: (String value) {
        int calories = int.tryParse(value);

        if (calories == null || calories <= 0) {
          return 'Calories must be greater than 0';
        }

        return null;
      },
      onSaved: (String value) {
        _calories = value;
      },
    );
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text("Form Demo")),
      body: Container(
        margin: EdgeInsets.all(24),
        child: Form(
          key: _formKey,
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: <Widget>[
              _buildName(),
              _buildEmail(),
              _buildPassword(),
              _builURL(),
              _buildPhoneNumber(),
              _buildCalories(),
              SizedBox(height: 100),
              RaisedButton(
                child: Text(
                  'Submit',
                  style: TextStyle(color: Colors.blue, fontSize: 16),
                ),
                onPressed: () {
                  if (!_formKey.currentState.validate()) {
                    return;
                  }

                  _formKey.currentState.save();

                  print(_name);
                  print(_email);
                  print(_phoneNumber);
                  print(_url);
                  print(_password);
                  print(_calories);

                  //Send to API
                },
              )
            ],
          ),
        ),
      ),
    );
  }
}
Sherlie
  • 81
  • 3
  • 12

4 Answers4

2

Use FocusNode with autovalidateMode: tempFocusNode.hasFocus? AutovalidateMode.always:AutovalidateMode.disabled as follows,in TextFormField this will solve your problem.

    import 'package:flutter/material.dart';

class FormScreen extends StatefulWidget {
  @override
  State<StatefulWidget> createState() {
    return FormScreenState();
  }
}

class FormScreenState extends State<FormScreen> {
  String _name;
  String _email;
  String _password;
  String _url;
  String _phoneNumber;
  String _calories;

  final GlobalKey<FormState> _formKey = GlobalKey<FormState>();

//create focusnodes here

FocusNode nameFocusNode= FocusNode();
FocusNode emailFocusNode= FocusNode();


  Widget _buildName() {
    return TextFormField(
focusNode:nameFocusNode,
autovalidateMode: nameFocusNode.hasFocus
                                ? AutovalidateMode.always
                                : AutovalidateMode.disabled,
      decoration: InputDecoration(labelText: 'Name'),
      maxLength: 10,
      validator: (String value) {
        if (value.isEmpty) {
          return 'Name is Required';
        }

        return null;
      },
      onSaved: (String value) {
        _name = value;
      },
    );
  }
............
  Widget _buildEmail() {
    return TextFormField(
    focusNode:emailFocusNode,
    autovalidateMode: nameFocusNode.hasFocus
                                    ? AutovalidateMode.always
                                    : AutovalidateMode.disabled,
      decoration: InputDecoration(labelText: 'Email'),
      validator: (String value) {
        if (value.isEmpty) {
          return 'Email is Required';
        }

        if (!RegExp(
                r"[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?")
            .hasMatch(value)) {
          return 'Please enter a valid email Address';
        }

        return null;
      },
      onSaved: (String value) {
        _email = value;
      },
    );
  }


  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text("Form Demo")),
      body: Container(
        margin: EdgeInsets.all(24),
        child: Form(
          key: _formKey,
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: <Widget>[
              _buildName(),
              _buildEmail(),
              _buildPassword(),
              _builURL(),
              _buildPhoneNumber(),
              _buildCalories(),
              SizedBox(height: 100),
              RaisedButton(
                child: Text(
                  'Submit',
                  style: TextStyle(color: Colors.blue, fontSize: 16),
                ),
                onPressed: () {
                  if (!_formKey.currentState.validate()) {
                    return;
                  }

                  _formKey.currentState.save();

                  print(_name);
                  print(_email);
                  print(_phoneNumber);
                  print(_url);
                  print(_password);
                  print(_calories);

                  //Send to API
                },
              )
            ],
          ),
        ),
      ),
    );
  }
}
Thusitha Deepal
  • 1,392
  • 12
  • 19
0

Form's validate() method will validate all the fields in it and it can't break. This is the source code in FormState.

bool _validate() {
  bool hasError = false;
  for (final FormFieldState<dynamic> field in _fields)
    hasError = !field.validate() || hasError;
  return !hasError;
}

If you want to validate the field one by one, you should use TextFormField's validate() manually.

import 'package:flutter/material.dart';

class FormScreen extends StatefulWidget {
  @override
  State<StatefulWidget> createState() {
    return FormScreenState();
  }
}

class FormScreenState extends State<FormScreen> {
  String _name;
  String _email;
  String _password;
  String _url;
  String _phoneNumber;
  String _calories;

  List<GlobalKey<FormFieldState>> fieldKeys;
  GlobalKey<FormFieldState> nameKey;
  GlobalKey<FormFieldState> emailKey;
  GlobalKey<FormFieldState> passwordKey;
  GlobalKey<FormFieldState> urlKey;
  GlobalKey<FormFieldState> phoneNumberKey;
  GlobalKey<FormFieldState> caloriesKey;

  @override
  void initState() {
    super.initState();
    nameKey = GlobalKey<FormFieldState>();
    emailKey = GlobalKey<FormFieldState>();
    passwordKey = GlobalKey<FormFieldState>();
    urlKey = GlobalKey<FormFieldState>();
    phoneNumberKey = GlobalKey<FormFieldState>();
    caloriesKey = GlobalKey<FormFieldState>();
    fieldKeys = [
      nameKey,
      emailKey,
      passwordKey,
      urlKey,
      phoneNumberKey,
      caloriesKey,
    ];
  }

  bool validate() {
    return fieldKeys.every((element) => element.currentState.validate());
  }

  void save() {
    fieldKeys.forEach((element) => element.currentState.save());
  }

  Widget _buildName() {
    return TextFormField(
      key: nameKey,
//  if you use autovalidateMode:AutovalidateMode.onUserInteractionit is showing validation message for the remaining fields too even before user starts entering the other fields 
//  then don't use any autovalidateMode.
//      autovalidateMode: AutovalidateMode.onUserInteraction,
      decoration: InputDecoration(labelText: 'Name'),
      maxLength: 10,
      validator: (String value) {
        if (value.isEmpty) {
          return 'Name is Required';
        }

        return null;
      },
      onSaved: (String value) {
        _name = value;
      },
    );
  }

  Widget _buildEmail() {
    return TextFormField(
      key: emailKey,
      decoration: InputDecoration(labelText: 'Email'),
      validator: (String value) {
        if (value.isEmpty) {
          return 'Email is Required';
        }

        if (!RegExp(
                r"[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?")
            .hasMatch(value)) {
          return 'Please enter a valid email Address';
        }

        return null;
      },
      onSaved: (String value) {
        _email = value;
      },
    );
  }

  Widget _buildPassword() {
    return TextFormField(
      key: passwordKey,
      decoration: InputDecoration(labelText: 'Password'),
      keyboardType: TextInputType.visiblePassword,
      validator: (String value) {
        if (value.isEmpty) {
          return 'Password is Required';
        }

        return null;
      },
      onSaved: (String value) {
        _password = value;
      },
    );
  }

  Widget _buildURL() {
    return TextFormField(
      key: urlKey,
      decoration: InputDecoration(labelText: 'Url'),
      keyboardType: TextInputType.url,
      validator: (String value) {
        if (value.isEmpty) {
          return 'URL is Required';
        }

        return null;
      },
      onSaved: (String value) {
        _url = value;
      },
    );
  }

  Widget _buildPhoneNumber() {
    return TextFormField(
      key: phoneNumberKey,
      decoration: InputDecoration(labelText: 'Phone number'),
      keyboardType: TextInputType.phone,
      validator: (String value) {
        if (value.isEmpty) {
          return 'Phone number is Required';
        }

        return null;
      },
      onSaved: (String value) {
        _url = value;
      },
    );
  }

  Widget _buildCalories() {
    return TextFormField(
      key: caloriesKey,
      decoration: InputDecoration(labelText: 'Calories'),
      keyboardType: TextInputType.number,
      validator: (String value) {
        int calories = int.tryParse(value);

        if (calories == null || calories <= 0) {
          return 'Calories must be greater than 0';
        }

        return null;
      },
      onSaved: (String value) {
        _calories = value;
      },
    );
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text("Form Demo")),
      body: Container(
        margin: EdgeInsets.all(24),
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            _buildName(),
            _buildEmail(),
            _buildPassword(),
            _buildURL(),
            _buildPhoneNumber(),
            _buildCalories(),
            SizedBox(height: 100),
            RaisedButton(
              child: Text(
                'Submit',
                style: TextStyle(color: Colors.blue, fontSize: 16),
              ),
              onPressed: () {
                if (!validate()) {
                  return;
                }

                save();

                print(_name);
                print(_email);
                print(_phoneNumber);
                print(_url);
                print(_password);
                print(_calories);

                //Send to API
              },
            )
          ],
        ),
      ),
    );
  }
}
zhpoo
  • 726
  • 4
  • 10
  • thanks! does this validate two text fields for example enter password and confirm password as the enter/while the user types – Sherlie Nov 25 '20 at 08:49
  • Of course! and I suggest that don't use `autovalidate: true`, this will always validate the field, you can change it to `autovalidateMode:AutovalidateMode.onUserInteraction,` – zhpoo Nov 25 '20 at 09:03
  • if i use autovalidateMode:AutovalidateMode.onUserInteractionit is showing validation messagefor the remaining fields too even before user starts entering the other fields – Sherlie Nov 25 '20 at 11:29
  • hey thanks can u give me a bit more clarity on what u meant. i applied these multiple keys to different widgets. but i got this error: Unhandled Exception: NoSuchMethodError: The method 'validate' was called on null. – Sherlie Nov 25 '20 at 13:38
0

You should be not use Form widget and TextFormField if you want to display error one at a time. Instead use validation by controllers.

For Example

class MyHomePage extends StatefulWidget {
  @override
  MyHomePageState createState() {
    return new MyHomePageState();
  }
}

class MyHomePageState extends State<MyHomePage> {
  final _text = TextEditingController();
  bool _validate = false;

  @override
  void dispose() {
    _text.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('TextField Demo'),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Text('Error Showed if Field is Empty on Submit button Pressed'),
            TextField(
              controller: _text,
              decoration: InputDecoration(
                labelText: 'Enter the Value',
              ),
            ),
            RaisedButton(
              onPressed: () {
                        if(_text.text.length<=5){
                    // open dialog
                  }
              },
              child: Text('Submit'),
              textColor: Colors.white,
              color: Colors.blueAccent,
            )
          ],
        ),
      ),
    );
  }
}
  • thanks for answering! im using form because i want this to validate two text fields for example enter password and confirm password as the enter/while the user types. Does the above code validate as the user types – Sherlie Nov 25 '20 at 08:51
  • No, Error Showed if Field is Empty on Submit button Pressed.. but you can modify the code to meet the requirements. –  Nov 25 '20 at 09:35
0

I really liked Thusithas idea but since the widget was not rebuilt when the focus was changed, the autoValidateMode was not changed either. So I created an observable variable hasFocus, integrated a listener on focus changes (see https://stackoverflow.com/a/68675855/17203788) and wrapped the widget with Obx so it would re-render on focus changes. In my implementation I used flutter_form_builder but the concept is also applicable to the classic Material form.

import 'package:flutter/material.dart';
import 'package:flutter_form_builder/flutter_form_builder.dart';
import 'package:get/get.dart';

class FormInputField extends StatelessWidget {
  /// Unique name of this input field
  final String name;

  /// Validator for this input field
  final FormFieldValidator<String>? validator;

  /// Observable boolean whether this text field currently has focus
  final RxBool _hasFocus = false.obs;

  FormInputField({
    required this.name,
    this.validator,
    Key? key,
  }) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Focus(
      onFocusChange: (hasFocus) {
        // Update observable
        _hasFocus(hasFocus);
      },
      child: Obx(() {
        return FormBuilderTextField(
          name: name,
          autovalidateMode: _hasFocus.value
              ? AutovalidateMode.always
              : AutovalidateMode.disabled,
          validator: validator,
        );
      }),
    );
  }
}

Alternatively you could also use a stateful widget and call setState in the onFocusChanged-callback.

Usage:

import 'package:flutter/material.dart';
import 'package:flutter_form_builder/flutter_form_builder.dart';

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

  @override
  _HomeState createState() => _HomeState();
}

class _HomeState extends State<Home> {
  /// Key to identify the form
  final GlobalKey<FormBuilderState> _formKey = GlobalKey();

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: FormBuilder(
        key: _formKey,
        child: Column(
          children: [
            FormInputField(
              name: 'username',
              validator: FormBuilderValidators.compose([
                FormBuilderValidators.required(
                  context,
                  errorText: 'Name is required',
                ),
              ]),
            ),
            FormInputField(
              name: 'email',
              validator: FormBuilderValidators.compose([
                FormBuilderValidators.required(
                  context,
                  errorText: 'Email is required',
                ),
                FormBuilderValidators.email(
                  context,
                  errorText: 'This is not a valid email',
                ),
              ]),
            ),
            ElevatedButton(
              onPressed: () {
                // Save and validate the form
                if (!_formKey.currentState!.saveAndValidate()) {
                  return;
                }

                // Extract values from form
                final Map<String, dynamic> values = _formKey.currentState!.value;

                String username = values['username'];
                String email = values['email'];

                print(username);
                print(email);
              },
              child: const Text('Submit'),
            ),
          ],
        ),
      ),
    );
  }
}