如何与TextEditingController、Form和TextFormField一起工作

446 阅读8分钟

是否需要使用TextField ,并在用户输入时对文本进行即时验证?

动态验证文本字段

在这个例子中,如果文本为空或太短,我们会显示一个自定义的错误提示禁用提交按钮

如果你想在Flutter中实现这个功能,你会怎么做?


StackOverflow上的人们似乎对此有很多看法,确实有两种主要的方法。

  1. 使用一个TextField 与一个TextEditingController 和一个ValueListenableBuilder 来更新UI。
  2. 使用一个Form ,一个TextFormField ,同时使用一个GlobalKey ,来验证和保存文本字段的数据。

在这篇文章中,我们将探讨这两种解决方案,以便你能学会如何在Flutter中处理文本输入。

1.用TextEditingController实现Flutter TextField验证

为了开始,让我们先建立基本的UI。

带有TextField和ElevatedButton的基本用户界面

第一步是创建一个StatefulWidget 子类,它将包含我们的TextField 和提交按钮。

class TextSubmitWidget extends StatefulWidget {
  const TextSubmitWidget({Key? key, required this.onSubmit}) : super(key: key);
  final ValueChanged<String> onSubmit;

  @override
  State<TextSubmitWidget> createState() => _TextSubmitWidgetState();
}

注意我们如何添加一个onSubmit 回调。我们将用它来通知父部件,当用户在输入有效文本后按下 "提交 "按钮。

接下来,让我们创建State 子类。

class _TextSubmitWidgetState extends State<TextSubmitWidget> {
  @override
  Widget build(BuildContext context) {
    return Column(
      mainAxisSize: MainAxisSize.min,
      crossAxisAlignment: CrossAxisAlignment.stretch,
      children: [
        TextField(
          decoration: InputDecoration(
            labelText: 'Enter your name',
            // TODO: add errorHint
          ),
        ),
        ElevatedButton(
          // TODO: implement callback
          onPressed: () {},
          child: Text(
            'Submit',
            style: Theme.of(context).textTheme.headline6,
          ),
        )
      ],
    );
  }
}

这是一个简单的Column 布局,包含一个TextField 和一个ElevatedButton

如果我们在一个单页的Flutter应用程序内运行这段代码,文本字段和提交按钮都会显示。

没有验证的Flutter TextField

接下来,我们要添加所有的验证逻辑,并根据这些规则来更新用户界面。

  • 如果文本是空的禁用提交按钮,并显示Can't be empty 作为错误提示
  • 如果文本不是空的,但太短,则启用提交按钮并显示Too short 作为错误提示
  • 如果文本足够长,则启用提交按钮并删除错误提示

让我们来想想如何实现这一点。

添加一个TextEditingController

Flutter给了我们一个TextEditingController类,我们可以用它来控制我们的文本区域。

所以让我们来使用它。我们所要做的就是在State 子类中创建它。

class _TextSubmitWidgetState extends State<TextSubmitWidget> {
  // create a TextEditingController
  final _controller = TextEditingController();

  // dispose it when the widget is unmounted
  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

  ...
}

然后我们可以把它传递给我们的TextField

TextField(
  // use this to control the text field
  controller: _controller,
  decoration: InputDecoration(
    labelText: 'Enter your name',
  ),
),

我们还可以添加一个getter变量来控制我们传递给TextField_errorText

String? get _errorText {
  // at any time, we can get the text from _controller.value.text
  final text = _controller.value.text;
  // Note: you can do your own custom validation here
  // Move this logic this outside the widget for more testable code
  if (text.isEmpty) {
    return 'Can\'t be empty';
  }
  if (text.length < 4) {
    return 'Too short';
  }
  // return null if the text is valid
  return null;
}

// then, in the build method:
TextField(
  controller: _controller,
  decoration: InputDecoration(
    labelText: 'Enter your name',
    // use the getter variable defined above
    errorText: _errorText,
  ),
),

有了这些,我们就可以在我们的按钮里面的onPressed 回调中添加一些自定义逻辑。

ElevatedButton(
  // only enable the button if the text is not empty
  onPressed: _controller.value.text.isNotEmpty
      ? _submit
      : null,
  child: Text(
    'Submit',
    style: Theme.of(context).textTheme.headline6,
  ),
)

注意我们如何在文本不是空的情况下调用一个_submit 方法。这个定义如下。

void _submit() {
  // if there is no error text
  if (_errorText == null) {
    // notify the parent widget via the onSubmit callback
    widget.onSubmit(_controller.value.text);
  }
}

但是如果我们运行这段代码,TextField 总是显示错误的文本,即使我们输入了有效的文本,提交按钮仍然无效。

TextField和提交按钮不更新

这到底是怎么回事?🧐

小工具重建和setState()

问题在于,我们没有告诉Flutter在文本发生变化时重建我们的小部件

我们可以通过添加一个本地状态变量来解决这个问题,并在我们的TextFieldonChanged 回调中通过调用setState() 来更新它。

// In the state class
var _text = '';

// inside the build method:
TextField(
  controller: _controller,
  decoration: InputDecoration(
    labelText: 'Enter your name',
    errorText: errorText,
  ),
  // this will cause the widget to rebuild whenever the text changes
  onChanged: (text) => setState(() => _text),
),

有了这个变化,我们的UI就会即时更新,并且表现得和预期的一样。

Flutter TextField验证现在可以正常工作了

但本地状态变量是没有必要的,因为我们的TextEditingController 已经持有文本值,因为它的变化。

为了证明这一点,我们可以对setState() 进行一次空调用,一切都会正常。

onChanged: (_) => setState(() {}),

但是像这样强行重建一个小部件似乎有点反常。一定有一个更好的方法。

如何使用TextEditingController和ValueListenableBuilder?

事实证明,我们可以用一个ValueListenableBuilder来包装我们的widget树,它把我们的TextEditingController 作为一个参数。

@override
Widget build(BuildContext context) {
  return ValueListenableBuilder(
    // Note: pass _controller to the animation argument
    valueListenable: _controller,
    builder: (context, TextEditingValue value, __) {
      // this entire widget tree will rebuild every time
      // the controller value changes
      return Column(
        mainAxisSize: MainAxisSize.min,
        crossAxisAlignment: CrossAxisAlignment.stretch,
        children: [
          TextField(
            controller: _controller,
            decoration: InputDecoration(
              labelText: 'Enter your name',
              errorText: _errorText,
            ),
          ),
          ElevatedButton(
            onPressed: _controller.value.text.isNotEmpty
                ? _submit
                : null,
            child: Text(
              'Submit',
              style: Theme.of(context).textTheme.headline6,
            ),
          )
        ],
      );
    },
  );
}

因此,当文本发生变化时,TextFieldElevatedButton 都会重新构建。

Flutter TextField验证仍然正常工作

但为什么我们可以将我们的TextEditingController 传递给ValueListenableBuilder 呢?

ValueListenableBuilder(
  // this is valid because TextEditingController implements Listenable
  valueListenable: _controller,
  builder: (context, TextEditingValue value, __) { ... }
)

嗯,ValueListenableBuilder 需要一个类型为ValueListenable<T> 的参数。

TextEditingController 扩展了ValueNotifier<TextEditingValue> ,它实现了ValueListenable<ValueListenable> 。这就是Flutter SDK中定义这些类的方式。

class TextEditingController extends ValueNotifier<TextEditingValue> { ... }
class ValueNotifier<T> extends ChangeNotifier implements ValueListenable<T> { ... }

所以在这里我们有了它。当TextEditingController 的值发生变化时,我们可以使用ValueListenableBuilder 来重建我们的用户界面。👍

关于验证用户体验的注意事项

上面的例子是可行的,但有一个缺点:在用户有机会输入任何文本之前,我们就马上显示了一个验证错误。

文本字段的即时验证

这不是好的用户体验。最好是在文本提交才显示任何错误。

我们可以通过添加一个_submitted 状态变量来解决这个问题,这个变量只有在提交按钮被按下时才会被设置为true

class _TextSubmitWidgetState extends State<TextSubmitWidget> {
  bool _submitted = false;

  void _submit() {
    setState(() => _submitted = true);
    if (_errorText == null) {
      widget.onSubmit(_controller.value.text);
    }
  }

  ...
}

然后,我们可以用它来有条件地显示错误文本。

TextField(
  controller: _controller,
  decoration: InputDecoration(
    labelText: 'Enter your name',
    // only show the error text if the form was submitted
    errorText: _submitted ? _errorText : null,
  ),
)

有了这个变化,错误文本只在我们提交表单后显示。

提交时的错误文本

好多了。

Flutter TextField Validation with TextEditingController:总结

这里是我们到目前为止所涉及的关键点。

  • 当我们处理文本输入时,我们可以使用TextEditingController获取 TextField 的值。
  • 如果我们想让我们的小部件在文本变化时重新构建,我们可以用一个ValueListenableBuilder ,把TextEditingController 作为一个参数来包裹它们。

这很有效,但设置起来有点麻烦。如果我们可以使用一些高级的API来管理表单验证,那不是很好吗?

这正是FormTextFormField小组件的作用。

因此,让我们通过用表单实现同样的解决方案来弄清楚如何使用它们。

2.使用TextFormField的Flutter表单验证

下面是一个使用_TextSubmitWidgetState 的替代实现,它使用的是Form

class _TextSubmitWidgetState extends State<TextSubmitForm> {
  // declare a GlobalKey
  final _formKey = GlobalKey<FormState>();
  // declare a variable to keep track of the input text
  String _name = '';

  void _submit() {
    // validate all the form fields
    if (_formKey.currentState!.validate()) {
      // on success, notify the parent widget
      widget.onSubmit(_name);
    }
  }

  @override
  Widget build(BuildContext context) {
    // build a Form widget using the _formKey created above.
    return Form(
      key: _formKey,
      child: Column(
        mainAxisSize: MainAxisSize.min,
        crossAxisAlignment: CrossAxisAlignment.stretch,
        children: [
          TextFormField(
            decoration: const InputDecoration(
              labelText: 'Enter your name',
            ),
            // use the validator to return an error string (or null) based on the input text
            validator: (text) {
              if (text == null || text.isEmpty) {
                return 'Can\'t be empty';
              }
              if (text.length < 4) {
                return 'Too short';
              }
              return null;
            },
            // update the state variable when the text changes
            onChanged: (text) => setState(() => _name = text),
          ),
          ElevatedButton(
            // only enable the button if the text is not empty
            onPressed: _name.isNotEmpty ? _submit : null,
            child: Text(
              'Submit',
              style: Theme.of(context).textTheme.headline6,
            ),
          ),
        ],
      ),
    );
  }
}

下面是上面的代码是如何工作的。

  1. 我们声明一个GlobalKey ,我们可以用它来访问表单状态,并把它作为一个参数传递给Form widget。
  2. 我们使用一个TextFormField ,而不是TextField
  3. 这需要一个validator 函数参数,我们可以用它来指定我们的验证逻辑。
  4. 我们使用一个单独的_name 状态变量,并在TextFormField widget的onChanged 回调中更新它(注意这是在ElevatedButtononPressed 回调中使用)。
  5. _submit() 方法中,我们调用_formKey.currentState!.validate() 来验证所有的表单数据。如果这是成功的,我们通过调用widget.onSubmit(_name) 来通知父窗口部件。

FlutterFormState类给了我们验证保存方法,使我们更容易管理表单数据。

自动验证模式(AutovalidateMode

为了决定何时进行TextFormField 验证,我们可以传递一个autovalidateMode 参数。这是一个enum ,定义如下。

/// Used to configure the auto validation of [FormField] and [Form] widgets.
enum AutovalidateMode {
  /// No auto validation will occur.
  disabled,

  /// Used to auto-validate [Form] and [FormField] even without user interaction.
  always,

  /// Used to auto-validate [Form] and [FormField] only after each user
  /// interaction.
  onUserInteraction,
}

默认情况下,使用AutovalidateMode.disabled

我们可以把它改为AutovalidateMode.onUserInteraction ,这样我们的TextFormField 在文本发生变化时就会验证。

TextFormField(
  decoration: const InputDecoration(
    labelText: 'Enter your name',
  ),
  // validate after each user interaction
  autovalidateMode: AutovalidateMode.onUserInteraction,
  // The validator receives the text that the user has entered.
  validator: (text) {
    if (text == null || text.isEmpty) {
      return 'Can\'t be empty';
    }
    if (text.length < 4) {
      return 'Too short';
    }
    return null;
  },
)

但正如我们之前所说,我们只想表单提交启用验证。

因此,让我们像之前那样添加一个_submitted 变量。

class _TextSubmitFormState extends State<TextSubmitForm> {
  final _formKey = GlobalKey<FormState>();
  String _name = '';
  // use this to keep track of when the form is submitted
  bool _submitted = false;

  void _submit() {
    // set this variable to true when we try to submit
    setState(() => _submitted = true);
    if (_formKey.currentState!.validate()) {
      _formKey.currentState!.save();
      widget.onSubmit(_name);
    }
  }
}

然后,在TextFormField 里面,我们可以这样做。

TextFormField(
  autovalidateMode: _submitted
      ? AutovalidateMode.onUserInteraction
      : AutovalidateMode.disabled,
)

最终的结果正是我们想要的:错误提示只在我们提交表单后显示,如果文本是无效的。

提交时的错误文本

总结

我们现在已经探索了在Flutter中验证表单的两种不同方法。

您可以在Dartpad上找到完整的源代码并玩一玩这两种解决方案。

您应该使用哪一个?

我推荐使用FormTextFormField,因为它们给你一些高级的API,使你更容易处理文本输入,而且如果你在同一个页面上有多个表单字段,它们更适合。

尽管如此,TextEditingController 给你更精细的控制,让你获得设置文本,当你需要预先填充一个文本字段时,这可能很方便。你可以在TextEditingController文档中找到更多细节。

如果您想了解更多关于表单的工作,请查看Flutter.dev的官方文档

编码愉快!