Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Flutter: Change state depending on validation

I am building a simple forgot password form for a demo app which consists of one TextFormFields and a FloatingActionButton to submit the data. I have realised that the FloatingActionButton doesn't have disabled boolean state as such, so I wanted to try and replicate it by change the state to _isValid: true/ false depending on the TextFormField validation functions, which I can then put some ternary operators on FloatingActionButton to change the color and the functionality, depending on the state of this widget.

You will be able to see that I have the _autoValidate set to true on mounting of the widget then I try and trigger a UI reload in the _validateForgetEmail function. When I trigger these state changes I get a big UI error saying

══╡ EXCEPTION CAUGHT BY WIDGETS LIBRARY ╞═══════════════════════════════════════════════════════════
flutter: The following assertion was thrown building Form-[LabeledGlobalKey<FormState>#0a40e](dirty, state:
flutter: FormState#59216):
flutter: setState() or markNeedsBuild() called during build.
flutter: This ForgotPasswordForm widget cannot be marked as needing to build because the framework is already
flutter: in the process of building widgets. A widget can be marked as needing to be built during the build
flutter: phase only if one of its ancestors is currently building. This exception is allowed because the
flutter: framework builds parent widgets before children, which means a dirty descendant will always be
flutter: built. Otherwise, the framework might not visit this widget during this build phase.

Code is below:

class ForgotPasswordForm extends StatefulWidget {
  @override
  _ForgotPasswordFormState createState() => _ForgotPasswordFormState();
}
Class _ForgotPasswordFormState extends State<ForgotPasswordForm> {
  final _emailController = TextEditingController();
  final _formKey = GlobalKey<FormState>();
  final bool _autoValidate = true;

  bool _isLoading = false;
  bool _isValid = false;

  String email;

  @override
  Widget build(BuildContext context) {
    // Build a Form widget using the _formKey created above.
    return Form(
      key: _formKey,
      child: _isLoading
          ? _buildLoadingSpinner(context)
          : _buildPasswordForm(context),
      autovalidate: _autoValidate,
    );
  }

  Widget _buildLoadingSpinner(BuildContext context) {
    return (Center(child: CircularProgressIndicator()));
  }

  Widget _buildPasswordForm(BuildContext context) {
    print('isValid: ' + _isValid.toString());

    return Column(
      children: <Widget>[
        Text(
          'Please enter your email address.',
          style: TextStyle(fontSize: 14.0),
          textAlign: TextAlign.center,
        ),
        Text(
          'You will recieve a link to reset your password.',
          style: TextStyle(fontSize: 14.0),
          textAlign: TextAlign.center,
        ),
        SizedBox(height: 32.0),
        TextFormField(
          controller: _emailController,
          validator: _validateForgetEmail,
          keyboardType: TextInputType.emailAddress,
          autovalidate: _autoValidate,
          style: TextStyle(fontSize: 14.0),
          onSaved: (String val) {
            email = val;
          },
          decoration: InputDecoration(
            filled: true,
            contentPadding: EdgeInsets.symmetric(horizontal: 15, vertical: 8),
            labelText: 'Email',
            border: InputBorder.none,
            labelStyle: TextStyle(fontSize: 14.0, color: Colors.lightBlueAccent),
            errorStyle: TextStyle(fontSize: 10.0, height: 0.5),
            focusedBorder: UnderlineInputBorder(
              borderSide: BorderSide(color: Colors.lightGreenAccent, width: 2.0),
            ),
          ),
        ),
        SizedBox(height: 24.0),
        FloatingActionButton(
          backgroundColor: _isValid ? Colors.lightBlue : Colors.grey,
          onPressed: () {
            _submitPasswordReset();
          },
          child: Icon(Icons.arrow_forward_ios, size: 14.0),
        )
      ],
      mainAxisAlignment: MainAxisAlignment.center,
    );
  }

  void _submitPasswordReset() async {
    if (_formKey.currentState.validate()) {
      setState(() {
        _isLoading = true;
      });

      UserPasswordResetRequest newPasswordRequest =
          new UserPasswordResetRequest(email: _emailController.text);

      http.Response response = await ApiService.queryPost(
          '/api/users/password-forgot',
          body: newPasswordRequest.toJson());

      final int statusCode = response.statusCode;

      if (statusCode == 400) {
        Scaffold.of(context).showSnackBar(SnackBar(
            content: Text('Wrong email or password'),
            duration: Duration(seconds: 3),
            backgroundColor: Colors.red));

        setState(() {
          _isLoading = false;
        });
      }

      if (statusCode == 200) {
        // setState(() {
        //   _isLoading = false;
        // });

        Navigator.push(
          context,
          MaterialPageRoute(builder: (context) => UserBackToLogin()),
        );
      }

      setState(() {
        _isLoading = false;
      });
    }
  }

  String _validateForgetEmail(String value) {
    String patttern =
        r"^((([a-z]|\d|[!#\$%&'\*\+\-\/=\?\^_`{\|}~]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+(\.([a-z]|\d|[!#\$%&'\*\+\-\/=\?\^_`{\|}~]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+)*)|((\x22)((((\x20|\x09)*(\x0d\x0a))?(\x20|\x09)+)?(([\x01-\x08\x0b\x0c\x0e-\x1f\x7f]|\x21|[\x23-\x5b]|[\x5d-\x7e]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(\\([\x01-\x09\x0b\x0c\x0d-\x7f]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]))))*(((\x20|\x09)*(\x0d\x0a))?(\x20|\x09)+)?(\x22)))@((([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.)+(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))$";
    RegExp regExp = new RegExp(patttern);
    if (value.length == 0) {
      return "Email is Required";
    } else if (!regExp.hasMatch(value)) {
      setState(() {
        _isValid = false;
      });

      return "Must be a valid email address";
    }

    print('value' + value);

    setState(() {
      _isValid = true;
    });

    return null;
  }
}

Any insight would be great to see what I am doing wrong - very new to flutter. If you need any more info, then I can provide.

Cheers Sam

like image 626
Sam Kelham Avatar asked Jul 22 '19 07:07

Sam Kelham


1 Answers

You can do it like this:
Split _validateForgetEmail method in two:

String _validateForgetEmail(String value) {
  if (value.length == 0) {
    return "Email is Required";
  } else if (!_isEmailValid(value)) {
    return "Must be a valid email address";
  }

  print('value' + value);

  return null;
}

bool _isEmailValid(String value) {
  String pattern =
      r"^((([a-z]|\d|[!#\$%&'\*\+\-\/=\?\^_`{\|}~]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+(\.([a-z]|\d|[!#\$%&'\*\+\-\/=\?\^_`{\|}~]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+)*)|((\x22)((((\x20|\x09)*(\x0d\x0a))?(\x20|\x09)+)?(([\x01-\x08\x0b\x0c\x0e-\x1f\x7f]|\x21|[\x23-\x5b]|[\x5d-\x7e]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(\\([\x01-\x09\x0b\x0c\x0d-\x7f]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]))))*(((\x20|\x09)*(\x0d\x0a))?(\x20|\x09)+)?(\x22)))@((([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.)+(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))$";
  RegExp regExp = new RegExp(pattern);

  return regExp.hasMatch(value);
}

Now these methods only validate values without affecting any state.

Listen to _emailController changes

@override
void initState() {
  super.initState();
  _emailController.addListener(() {
    final isEmailValid = _isEmailValid(_emailController.value.text);
    if(isEmailValid != _isValid) {
      setState(() {
        _isValid = isEmailValid;
      });
    }
  });
}

Also don't forget to dispose _emailController

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

Exception explanation:
TextFormField extends FormField class. If autovalidate is turned on, then function passed as validator will be called in FormFieldState.build method to update error text.
So it leads to setState being called from build which is not allowed by framework

like image 101
Mikhail Ponkin Avatar answered Nov 03 '22 09:11

Mikhail Ponkin