Flutter form validation not working ( _formKey.currentState.save())

3,766

I've tried the minimal repro you've provided and the only thing missing is the expected String return value on TextFormField(validator: (value) { }) to display the error.

Here's a modified version of the provided minimal repro adding a return String value on the validator.

class SignForm extends StatefulWidget {
  @override
  _SignFormState createState() => _SignFormState();
}

class _SignFormState extends State<SignForm> {
  // GlobalKey This uniquely identifies the Form , and allows validation of the form in a later step.
  final _formKey = GlobalKey<FormState>();
  String? email, password;
  bool remember = false;
  final List<String> errors = [];

// func with named parameter
  void addError({required String error}) {
    if (!errors.contains(error))
      setState(() {
        errors.add(error);
      });
  }

  void removeError({required String error}) {
    if (errors.contains(error))
      setState(() {
        errors.remove(error);
      });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Form(
        key: _formKey,
        child: Column(
          children: [
            // TextFormField - Creates a [FormField] that contains a [TextField].
            buildEmailFormField(),
            SizedBox(height: 30.0),
            buildPasswordFormField(),
            SizedBox(height: 30.0),
            Row(
              children: [
                Checkbox(
                  value: remember,
                  onChanged: (value) {
                    setState(() {
                      remember = value ?? false;
                    });
                  },
                ),
                Text("Remember me"),
                Spacer(),
                GestureDetector(
                  // onTap: () => Navigator.pushNamed(
                  //     context, ForgotPasswordScreen.routeName),
                  child: Text(
                    "Forgot Password?",
                    style: TextStyle(decoration: TextDecoration.underline),
                  ),
                )
              ],
            ),
            // FormError(errors: errors),
            SizedBox(height: 30.0),
            ElevatedButton(
              child: Text('Login'),
              onPressed: () {
                if (_formKey.currentState!.validate()) {
                  _formKey.currentState!.save();
                  // if all are valid then go to success screen
                  // Navigator.pushReplacementNamed(
                  // context, LoginSuccessScreen.routeName);

                }
                errors.forEach((String error) {
                  debugPrint('Error from Form: $error');
                });
              },
            )
          ],
        ),
      ),
    );
  }

  TextFormField buildPasswordFormField() {
    return TextFormField(
      // obscure visibility of the password
      obscureText: true,
      onSaved: (newValue) => password = newValue,
      onChanged: (value) {
        if (value.isNotEmpty && errors.contains('kPassNullError')) {
          removeError(error: 'kPassNullError');
        } else if (value.length >= 8) {
          removeError(error: 'kShortPassError');
        }
        // In case a user removed some characters below the threshold, show alert
        else if (value.length < 8 && value.isNotEmpty) {
          addError(error: 'kShortPassError');
        }
        return null;
      },
      validator: (value) {
        if (value == null || value.isEmpty) {
          addError(error: 'kPassNullError');
          removeError(error: 'kShortPassError');
          return 'Pass Empty';
        } else if (value.length < 8 && value.isNotEmpty) {
          addError(error: 'kShortPassError');
          return 'Short Pass';
        }
        return null;
      },
      decoration: InputDecoration(
        // uses the InputDecorationTheme defined in my theme.dart file
        labelText: "Password",
        hintText: "Enter your password",
        // When [FloatingLabelBehavior.always] the label will always float at the top of the field above the content.
        floatingLabelBehavior: FloatingLabelBehavior.always,
        // suffixIcon: CustomSuffixIcon(
        //   svgIcon: "assets/icons/Lock.svg",
        // ),
      ),
    );
  }

  TextFormField buildEmailFormField() {
    return TextFormField(
      // Requests a keyboard with ready access to the "@" and "." keys.
      keyboardType: TextInputType.emailAddress,
      onSaved: (newValue) => email = newValue,
      onChanged: (value) {
        if (value.isNotEmpty && errors.contains('kEmailNullError')) {
          removeError(error: 'kEmailNullError');
        }
        // else if (emailValidatorRegExp.hasMatch(value)) {
        //   removeError(error: 'kInvalidEmailError');
        // }
        else if (value.isNotEmpty) {
          addError(error: 'kInvalidEmailError');
          return null;
        }
      },
      validator: (value) {
        if (value == null || value.isEmpty) {
          addError(error: 'kEmailNullError');
          removeError(error: 'kInvalidEmailError');
          return 'Email Empty';
        } else if (value.isNotEmpty) {
          addError(error: 'kInvalidEmailError');
        }
        return null;
      },
      decoration: InputDecoration(
        // uses the InputDecorationTheme defined in my theme.dart file
        labelText: "Email",
        hintText: "Enter your email",
        // When [FloatingLabelBehavior.always] the label will always float at the top of the field above the content.
        floatingLabelBehavior: FloatingLabelBehavior.always,
        // suffixIcon: CustomSuffixIcon(
        //   svgIcon: "assets/icons/Mail.svg",
        // ),
      ),
    );
  }
}

Demo

Share:
3,766
Shadow Walker
Author by

Shadow Walker

Updated on December 17, 2022

Comments

  • Shadow Walker
    Shadow Walker over 1 year

    I have a Sign In form validator that is not working as expected. When I leave the email or password fields empty or enter something that is not accepted based on what I set on the validator the error is shown but I'm directed to login success page when I click the login button, which should not be the case. I want to stay on the sign in page until I enter the correct values.

    Sign In form

    class SignForm extends StatefulWidget {
      @override
      _SignFormState createState() => _SignFormState();
    }
    
    class _SignFormState extends State<SignForm> {
      // GlobalKey This uniquely identifies the Form , and allows validation of the form in a later step.
      final _formKey = GlobalKey<FormState>();
      String email, password;
      bool remember = false;
      final List<String> errors = [];
    
    // func with named parameter
      void addError({String error}) {
        if (!errors.contains(error))
          setState(() {
            errors.add(error);
          });
      }
    
      void removeError({String error}) {
        if (errors.contains(error))
          setState(() {
            errors.remove(error);
          });
      }
    
      @override
      Widget build(BuildContext context) {
        return Form(
          key: _formKey,
          child: Column(
            children: [
              // TextFormField - Creates a [FormField] that contains a [TextField].
              buildEmailFormField(),
              SizedBox(height: getProportionateScreenHeight(30)),
              buildPasswordFormField(),
              SizedBox(height: getProportionateScreenHeight(30)),
              Row(
                children: [
                  Checkbox(
                    value: remember,
                    activeColor: kPrimaryColor,
                    onChanged: (value) {
                      setState(() {
                        remember = value;
                      });
                    },
                  ),
                  Text("Remember me"),
                  Spacer(),
                  GestureDetector(
                    onTap: () => Navigator.pushNamed(
                        context, ForgotPasswordScreen.routeName),
                    child: Text(
                      "Forgot Password?",
                      style: TextStyle(decoration: TextDecoration.underline),
                    ),
                  )
                ],
              ),
              FormError(errors: errors),
              SizedBox(height: getProportionateScreenHeight(20)),
              DefaultButton(                                      < -- WHERE I THINK THE ERROR IS
                text: 'Login',
                press: () {
                  if (_formKey.currentState.validate()) {
                    _formKey.currentState.save();                  < -- WHERE I THINK THE ERROR IS
                    // if all are valid then go to success screen
                    Navigator.pushReplacementNamed(
                        context, LoginSuccessScreen.routeName);
                  }
                },
              )
            ],
          ),
        );
      }
    
      TextFormField buildPasswordFormField() {
        return TextFormField(
          // obscure visibility of the password
          obscureText: true,
          onSaved: (newValue) => password = newValue,
          onChanged: (value) {
            if (value.isNotEmpty && errors.contains(kPassNullError)) {
              removeError(error: kPassNullError);
            } else if (value.length >= 8) {
              removeError(error: kShortPassError);
            }
            // In case a user removed some characters below the threshold, show alert
            else if (value.length < 8 && value.isNotEmpty) {
              addError(error: kShortPassError);
            }
            return null;
          },
          validator: (value) {
            if (value.isEmpty) {
              addError(error: kPassNullError);
              removeError(error: kShortPassError);
            } else if (value.length < 8 && value.isNotEmpty) {
              addError(error: kShortPassError);
            }
            return null;
          },
          decoration: InputDecoration(
            // uses the InputDecorationTheme defined in my theme.dart file
            labelText: "Password",
            hintText: "Enter your password",
            // When [FloatingLabelBehavior.always] the label will always float at the top of the field above the content.
            floatingLabelBehavior: FloatingLabelBehavior.always,
            suffixIcon: CustomSuffixIcon(
              svgIcon: "assets/icons/Lock.svg",
            ),
          ),
        );
      }
    
      TextFormField buildEmailFormField() {
        return TextFormField(
          // Requests a keyboard with ready access to the "@" and "." keys.
          keyboardType: TextInputType.emailAddress,
          onSaved: (newValue) => email = newValue,
          onChanged: (value) {
            if (value.isNotEmpty && errors.contains(kEmailNullError)) {
              removeError(error: kEmailNullError);
            } else if (emailValidatorRegExp.hasMatch(value)) {
              removeError(error: kInvalidEmailError);
            } else if (value.isNotEmpty && !emailValidatorRegExp.hasMatch(value)) {
              addError(error: kInvalidEmailError);
              return null;
            }
          },
          validator: (value) {
            if (value.isEmpty) {
              addError(error: kEmailNullError);
              removeError(error: kInvalidEmailError);
            } else if (value.isNotEmpty && !emailValidatorRegExp.hasMatch(value)) {
              addError(error: kInvalidEmailError);
            }
            return null;
          },
          decoration: InputDecoration(
            // uses the InputDecorationTheme defined in my theme.dart file
            labelText: "Email",
            hintText: "Enter your email",
            // When [FloatingLabelBehavior.always] the label will always float at the top of the field above the content.
            floatingLabelBehavior: FloatingLabelBehavior.always,
            suffixIcon: CustomSuffixIcon(
              svgIcon: "assets/icons/Mail.svg",
            ),
          ),
        );
      }
    }