是否需要使用TextField ,并在用户输入时对文本进行即时验证?
动态验证文本字段
在这个例子中,如果文本为空或太短,我们会显示一个自定义的错误提示并禁用提交按钮。
如果你想在Flutter中实现这个功能,你会怎么做?
StackOverflow上的人们似乎对此有很多看法,确实有两种主要的方法。
- 使用一个
TextField与一个TextEditingController和一个ValueListenableBuilder来更新UI。 - 使用一个
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在文本发生变化时重建我们的小部件。
我们可以通过添加一个本地状态变量来解决这个问题,并在我们的TextField 的onChanged 回调中通过调用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,
),
)
],
);
},
);
}
因此,当文本发生变化时,TextField 和ElevatedButton 都会重新构建。
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来管理表单验证,那不是很好吗?
这正是Form和TextFormField小组件的作用。
因此,让我们通过用表单实现同样的解决方案来弄清楚如何使用它们。
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,
),
),
],
),
);
}
}
下面是上面的代码是如何工作的。
- 我们声明一个
GlobalKey,我们可以用它来访问表单状态,并把它作为一个参数传递给Formwidget。 - 我们使用一个
TextFormField,而不是TextField。 - 这需要一个
validator函数参数,我们可以用它来指定我们的验证逻辑。 - 我们使用一个单独的
_name状态变量,并在TextFormFieldwidget的onChanged回调中更新它(注意这是在ElevatedButton的onPressed回调中使用)。 - 在
_submit()方法中,我们调用_formKey.currentState!.validate()来验证所有的表单数据。如果这是成功的,我们通过调用widget.onSubmit(_name)来通知父窗口部件。
自动验证模式(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上找到完整的源代码并玩一玩这两种解决方案。
您应该使用哪一个?
我推荐使用Form和TextFormField,因为它们给你一些高级的API,使你更容易处理文本输入,而且如果你在同一个页面上有多个表单字段,它们更适合。
尽管如此,TextEditingController 给你更精细的控制,让你获得和设置文本,当你需要预先填充一个文本字段时,这可能很方便。你可以在TextEditingController文档中找到更多细节。
如果您想了解更多关于表单的工作,请查看Flutter.dev的官方文档。
编码愉快!