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

15.6k views Asked by At

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",
        ),
      ),
    );
  }
}

3

There are 3 answers

0
msalihbindak On

Changing from final GlobalKey _formKey = GlobalKey<FormState>(); to final _formKey = GlobalKey<FormState>(); solved my problem.

0
Omatt On

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

0
ajhavery On

The reason for this is that there is no return statement. If you add a return statement to the validator, then it will skip the execution of the function.

The below code will work as intended:

Option # 1:

  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);
      });
  }

  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.isEmpty) {
          removeError(error: kInvalidEmailError);
          addError(error: kEmailNullError);
        } else if (value.isNotEmpty) {
          removeError(error: kEmailNullError);
          if (emailValidatorRegExp.hasMatch(value)) {
            removeError(error: kInvalidEmailError);
          }
        }
      },
      validator: (value) {
        if (value == null || value.isEmpty) {
          addError(error: kEmailNullError);
          return "";
        } else if (value.isNotEmpty && 
     !emailValidatorRegExp.hasMatch(value)) {
          addError(error: kInvalidEmailError);
          return "";
        }
        return null;
      },
      decoration: const InputDecoration(
        labelText: "Email",
        hintText: "Enter your email",
        suffixIcon: CustomSuffixIcon(
          svgIcon: "assets/icons/Mail.svg",
        ),
      ),
    );
  }

  TextFormField buildPasswordFormField() {
    return TextFormField(
      obscureText: true,
      onSaved: (newValue) => password = newValue,
      onChanged: (value) {
        if (value.isEmpty) {
          removeError(error: kShortPassError);
          addError(error: kPassNullError);
        } else if (value.isNotEmpty) {
          removeError(error: kPassNullError);
          if (value.length >= 8) {
            removeError(error: kShortPassError);
          }
        }
      },
      validator: (value) {
        if (value == null || value.isEmpty) {
          addError(error: kPassNullError);
          return "";
        } else if (value.isNotEmpty && value.length < 8) {
          addError(error: kShortPassError);
          return "";
        }
        return null;
      },
      decoration: const InputDecoration(
        labelText: "Password",
        hintText: "Enter your password",
        suffixIcon: CustomSuffixIcon(
          svgIcon: "assets/icons/Lock.svg",
        ),
      ),
    );
  }
}

Option # 2: In the Button onPress logic: Check if:

errors == []