【问题标题】:Flutter: Change state depending on validationFlutter:根据验证更改状态
【发布时间】:2019-11-30 03:20:43
【问题描述】:

我正在为一个演示应用程序构建一个简单的忘记密码表单,其中包含一个TextFormFields 和一个FloatingActionButton 来提交数据。我已经意识到FloatingActionButton 没有禁用布尔状态,所以我想尝试通过将状态更改为_isValid: true/ false 来复制它,具体取决于TextFormField 验证函数,然后我可以放置一些三元FloatingActionButton 上的运算符来更改颜色和功能,具体取决于此小部件的状态。

您将能够看到我在安装小部件时将_autoValidate 设置为true,然后我尝试在_validateForgetEmail 函数中触发UI 重新加载。当我触发这些状态更改时,我会收到一个很大的 UI 错误提示

══╡ 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.

代码如下:

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

任何见解都会很高兴看到我做错了什么 - 非常新的颤振。如果您需要更多信息,我可以提供。

山姆干杯

【问题讨论】:

  • 从 _validateForgetEmail 中删除 setState 仅保留 - _isValid = false;
  • @anmol.majhail 但这不会触发此小部件的 UI 重建
  • 禁用按钮集onPressed: null。你可以这样做onPressed: isValid ? _submitPasswordReset : null

标签: mobile flutter dart material-design


【解决方案1】:

晚了一点,但您可以通过在验证器中添加后帧回调直接在验证器内部使用 setState,如下所示:

validator: (val) {
  if (val == '') {
    SchedulerBinding.instance.addPostFrameCallback((duration) {
      setState(() {
        someErrorVariableYouWantToChange = true;
      });
    });
  } else {
    SchedulerBinding.instance.addPostFrameCallback((duration) {
      setState(() {
        someErrorVariableYouWantToChange = false;
      });
    });
  }

  return null;

}

这将确保由于验证器功能,在构建过程完成后调用 setState。

【讨论】:

    【解决方案2】:

    简单使用

    Form(
       key: _key,
       autovalidateMode: AutovalidateMode.onUserInteraction,
       child: Column(
          children: [
             //All FormTextFields Here as you went....
          ],
       ),
    ),
    

    【讨论】:

      【解决方案3】:

      实现此目的的更简单方法是在 onChanged 回调中进行验证。

      class FormPage extends StatefulWidget {
        @override
        _FormPageState createState() => _FormPageState();
      }
      
      class _FormPageState extends State<FormPage> {
        final _formKey = GlobalKey<FormState>();
      
        bool _isValid = false;
      
        @override
        Widget build(BuildContext context) {
          return Scaffold(
            appBar: AppBar(),
            body: Form(
              key: _formKey,
              onChanged: () {
                final isValid = _formKey.currentState.validate();
                if (_isValid != isValid) {
                  setState(() {
                    _isValid = isValid;
                  });
                }
              },
              child: Column(
                children: <Widget>[
                  TextFormField(validator: (x) => x.length > 2 ? null : 'Too short'),
                ],
              ),
            ),
            floatingActionButton: Opacity(
              opacity: _isValid ? 1 : 0.5,
              child: FloatingActionButton(
                child: Icon(Icons.send),
                onPressed: () {},
              ),
            ),
          );
        }
      }
      
      

      【讨论】:

        【解决方案4】:

        你可以这样做:
        将_validateForgetEmail方法一分为二:

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

        现在这些方法只验证值而不影响任何状态。

        收听_emailController的变化

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

        另外别忘了处置_emailController

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

        异常说明:
        TextFormField 扩展了FormField 类。如果autovalidate 开启,则作为validator 传递的函数将在FormFieldState.build 方法中调用以更新错误文本。
        所以它导致setState 被build 调用,这是框架不允许的

        【讨论】:

        • 恕我直言,为了完整起见,有必要解释为什么不能在验证器中调用setState。
        • 嘿伙计 - 谢谢你的回答!很好的解决方案并且有效(并且教会了我更多关于 Flutter 的知识):)
        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 2021-12-09
        • 2019-12-08
        • 2019-08-21
        • 1970-01-01
        • 2021-09-25
        • 2017-11-27
        • 1970-01-01
        相关资源
        最近更新 更多