在验证器之外更新 TextFormField 的错误

Posted

技术标签:

【中文标题】在验证器之外更新 TextFormField 的错误【英文标题】:Update TextFormField's error outside of the validator 【发布时间】:2020-03-25 02:18:34 【问题描述】:

根据 TextFormField 文档,在 TextFormField 下方显示错误的唯一方法是从验证器函数返回错误字符串。但是,我有一个文本输入,只能在调用服务器后进行验证,并且服务器的响应(如果有效)也需要稍后使用。因此,我只在用户按下提交时才这样做。但是,如果服务器返回无效响应,我需要更新错误文本,但由于我在验证器之外,所以我不能这样做。

我接近这个了吗?有没有办法做到这一点?

TextFormField(
    autofocus: true,
    onSaved: (String value) => passcode = value,
),
SizedBox(50.0),
RaisedButtton(
    child: Text('SUBMIT'),
    onPressed: () async 
        _formKey.currentState.save();

        dynamic response = await someServerCall();

        if (response.token) 
            // Valid, use token
         else 
            // INVALID, update error text somehow
        
    
)

(这里的每样东西都有不同的父母,包括Column和Form,但这基本上是我正在做的)

【问题讨论】:

【参考方案1】:

您可以使用flutter_form_bloc。

每个字段都有addError方法,你可以在任何地方调用,在你的情况下,它会在收到服务器的响应后在onSubmitting方法中。

class MyFormBloc extends FormBloc<String, String> 
  final email = TextFieldBloc();

  MyFormBloc() 
    addFieldBlocs(fieldBlocs: [email]);
  

  @override
  void onSubmitting() async 
   // Awesome logic...
   username.addError('That email is taken. Try another.');
  


您还可以使用具有去抖动时间的异步验证器

class MyFormBloc extends FormBloc<String, String> 
  final username = TextFieldBloc(
    asyncValidatorDebounceTime: Duration(milliseconds: 300),
  );

  MyFormBloc() 
    addFieldBlocs(fieldBlocs: [username]);

    username.addAsyncValidators([_checkUsername]);
  

  Future<String> _checkUsername(String username) async 
    await Future.delayed(Duration(milliseconds: 500));
    if (username.toLowerCase() != 'flutter dev') 
      return 'That username is already taken';
    
    return null;
  

这是一个可以运行的小演示,教程在form bloc website

pubspec.yaml

dependencies:
  flutter_form_bloc: ^0.11.0

main.dart

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

void main() => runApp(App());

class App extends StatelessWidget 
  const App(Key key) : super(key: key);

  @override
  Widget build(BuildContext context) 
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      home: SubmissionErrorToFieldForm(),
    );
  


class SubmissionErrorToFieldFormBloc extends FormBloc<String, String> 
  final username = TextFieldBloc();

  SubmissionErrorToFieldFormBloc() 
    addFieldBlocs(
      fieldBlocs: [
        username,
      ],
    );
  

  @override
  void onSubmitting() async 
    print(username.value);

    await Future<void>.delayed(Duration(milliseconds: 500));

    if (username.value.toLowerCase() == 'dev') 
      username.addError(
        'Cached - That username is taken. Try another.',
        isPermanent: true,
      );

      emitFailure(failureResponse: 'Cached error was added to username field.');
     else 
      username.addError('That username is taken. Try another.');

      emitFailure(failureResponse: 'Error was added to username field.');
    
  


class SubmissionErrorToFieldForm extends StatelessWidget 
  @override
  Widget build(BuildContext context) 
    return BlocProvider(
      create: (context) => SubmissionErrorToFieldFormBloc(),
      child: Builder(
        builder: (context) 
          final formBloc =
              BlocProvider.of<SubmissionErrorToFieldFormBloc>(context);

          return Theme(
            data: Theme.of(context).copyWith(
              inputDecorationTheme: InputDecorationTheme(
                border: OutlineInputBorder(
                  borderRadius: BorderRadius.circular(20),
                ),
              ),
            ),
            child: Scaffold(
              appBar: AppBar(title: Text('Submission Error to Field')),
              body: FormBlocListener<SubmissionErrorToFieldFormBloc, String,
                  String>(
                onSubmitting: (context, state) 
                  LoadingDialog.show(context);
                ,
                onSuccess: (context, state) 
                  LoadingDialog.hide(context);

                  Navigator.of(context).pushReplacement(
                      MaterialPageRoute(builder: (_) => SuccessScreen()));
                ,
                onFailure: (context, state) 
                  LoadingDialog.hide(context);

                  Scaffold.of(context).showSnackBar(
                      SnackBar(content: Text(state.failureResponse)));
                ,
                child: SingleChildScrollView(
                  physics: ClampingScrollPhysics(),
                  child: Padding(
                    padding: const EdgeInsets.all(8.0),
                    child: Column(
                      children: <Widget>[
                        TextFieldBlocBuilder(
                          textFieldBloc: formBloc.username,
                          keyboardType: TextInputType.multiline,
                          decoration: InputDecoration(
                            labelText: 'Username',
                            prefixIcon: Icon(Icons.sentiment_very_satisfied),
                          ),
                        ),
                        Padding(
                          padding: const EdgeInsets.all(8.0),
                          child: Text('"dev" will add a cached error'),
                        ),
                        RaisedButton(
                          onPressed: formBloc.submit,
                          child: Text('SUBMIT'),
                        ),
                      ],
                    ),
                  ),
                ),
              ),
            ),
          );
        ,
      ),
    );
  


class LoadingDialog extends StatelessWidget 
  static void show(BuildContext context, Key key) => showDialog<void>(
        context: context,
        useRootNavigator: false,
        barrierDismissible: false,
        builder: (_) => LoadingDialog(key: key),
      ).then((_) => FocusScope.of(context).requestFocus(FocusNode()));

  static void hide(BuildContext context) => Navigator.pop(context);

  LoadingDialog(Key key) : super(key: key);

  @override
  Widget build(BuildContext context) 
    return WillPopScope(
      onWillPop: () async => false,
      child: Center(
        child: Card(
          child: Container(
            width: 80,
            height: 80,
            padding: EdgeInsets.all(12.0),
            child: CircularProgressIndicator(),
          ),
        ),
      ),
    );
  


class SuccessScreen extends StatelessWidget 
  SuccessScreen(Key key) : super(key: key);

  @override
  Widget build(BuildContext context) 
    return Scaffold(
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Icon(Icons.tag_faces, size: 100),
            SizedBox(height: 10),
            Text(
              'Success',
              style: TextStyle(fontSize: 54, color: Colors.black),
              textAlign: TextAlign.center,
            ),
            SizedBox(height: 10),
            RaisedButton.icon(
              onPressed: () => Navigator.of(context).pushReplacement(
                  MaterialPageRoute(
                      builder: (_) => SubmissionErrorToFieldForm())),
              icon: Icon(Icons.replay),
              label: Text('AGAIN'),
            ),
          ],
        ),
      ),
    );
  


【讨论】:

【参考方案2】:

添加一个布尔检查,您将根据服务器调用进行更改

TextFormField(
               validator: (value) 
                   if (hasErrorAfterServerCall)
                          return 'Your Error Message';
                   else
                          return null;
                ,
);

服务器调用完成后,您可以再次验证表单

_formKey.currentState!.validate();

【讨论】:

【参考方案3】:

为什么不在验证器函数中添加服务器调用。 在 TextFormField 中使用验证器,如下所示,

TextFormField( 
                validator: _validateEmail,
                  onSaved: (String value) 
                    email = value;
                  ,
                ),


  String _validateEmail(String value) async 
//call to a server inside a validator function
        dynamic response = await someServerCall();
         String  _token="";
        if (response.token) 
            // Valid, use token

           setState(()
       _token = response.token
            );
            return null;
         else 
            // INVALID, update error text somehow
            return "error";
        

  

如果验证器为空,那么它不会显示任何错误,但如果它得到任何字符串,那么它将将该字符串显示为错误。 现在是按钮


RaisedButtton(
    child: Text('SUBMIT'),
    onPressed: ()
       if (_formKey.currentState.validate()) _formKey.currentState.save();
)

【讨论】:

之后我将如何使用令牌? _validateEmail 在这里进行(昂贵的)服务器调用,但无法将令牌恢复到可用的上下文中。 令牌如果经过验证可以存储。我已经编辑了答案。 另外,验证器不能是异步的。如果将自己标记为异步,则返回 Future&lt;String&gt;,不能只返回 String 好的,有一个解决方案,但有点棘手。您可以直接点击服务器并等待响应,然后调用验证器。您也可以应用预检查来检查密码是否太短、电子邮件是否包含@xxx.xx 等。在调用查询之前。如果这些错误仍然存​​在,则直接调用验证器并通过它抛出错误。

以上是关于在验证器之外更新 TextFormField 的错误的主要内容,如果未能解决你的问题,请参考以下文章

用户交互后自动验证 TextFormField

TextFormField 验证器参数类型“动态函数()”不能分配给

AlertDialog 中的 TextFormField 未验证

如何使用 GetX 实现 Flutter TextFormField 验证器?

在 Flutter 中仅允许 TextFormField 中的特定输入而无需验证

Flutter:TextFormField Validator 打破了字段的样式