实际业务中,在正式向服务器提交数据前,都会对各个输入框数据进行合法性校验,但是对每一个TextField
都分别进行校验将会是一件很麻烦的事。还有,如果用户想清除一组TextField
的内容,除了一个一个清除有没有什么更好的办法呢?为此,Flutter提供了一个Form
组件,它可以对输入框进行分组,然后进行一些统一操作,如输入内容校验、输入框重置以及输入内容保存。
Form
Form
继承自StatefulWidget
对象,它对应的状态类为FormState
。我们先看看Form
类的定义:
Form({
@required Widget child,
bool autovalidate = false,
WillPopCallback onWillPop,
VoidCallback onChanged,
})
-
autovalidate
:是否自动校验输入内容;当为true
时,每一个子FormField内容发生变化时都会自动校验合法性,并直接显示错误信息。否则,需要通过调用FormState.validate()
来手动校验。 -
onWillPop
:决定Form
所在的路由是否可以直接返回(如点击返回按钮),该回调返回一个Future
对象,如果Future的最终结果是false
,则当前路由不会返回;如果为true
,则会返回到上一个路由。此属性通常用于拦截返回按钮。 -
onChanged
:Form
的任意一个子FormField
内容发生变化时会触发此回调。
FormField
Form
的子孙元素必须是FormField
类型,FormField
是一个抽象类,定义几个属性,FormState
内部通过它们来完成操作,FormField
部分定义如下:
const FormField({
...
FormFieldSetter onSaved, //保存回调
FormFieldValidator validator, //验证回调
T initialValue, //初始值
bool autovalidate = false, //是否自动校验。
})
为了方便使用,Flutter提供了一个TextFormField
组件,它继承自FormField
类,也是TextField
的一个包装类,所以除了FormField
定义的属性之外,它还包括TextField
的属性。
FormState
FormState
为Form
的State
类,可以通过Form.of()
或GlobalKey
获得。我们可以通过它来对Form
的子孙FormField
进行统一操作。我们看看其常用的三个方法:
-
FormState.validate()
:调用此方法后,会调用Form
子孙FormField的validate
回调,如果有一个校验失败,则返回false,所有校验失败项都会返回用户返回的错误提示。 -
FormState.save()
:调用此方法后,会调用Form
子孙FormField
的save
回调,用于保存表单内容 -
FormState.reset()
:调用此方法后,会将子孙FormField
的内容清空。
示例
我们修改一下上面用户登录的示例,在提交之前校验:
- 用户名不能为空,如果为空则提示“用户名不能为空”。
- 密码不能小于6位,如果小于6为则提示“密码不能少于6位”。
完整代码:
class FormTestRoute extends StatefulWidget {
@override
_FormTestRouteState createState() => new _FormTestRouteState();
}
class _FormTestRouteState extends State {
TextEditingController _unameController = new TextEditingController();
TextEditingController _pwdController = new TextEditingController();
GlobalKey _formKey= new GlobalKey();
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title:Text("Form Test"),
),
body: Padding(
padding: const EdgeInsets.symmetric(vertical: 16.0, horizontal: 24.0),
child: Form(
key: _formKey, //设置globalKey,用于后面获取FormState
autovalidate: true, //开启自动校验
child: Column(
children: [
TextFormField(
autofocus: true,
controller: _unameController,
decoration: InputDecoration(
labelText: "用户名",
hintText: "用户名或邮箱",
icon: Icon(Icons.person)
),
// 校验用户名
validator: (v) {
return v
.trim()
.length > 0 ? null : "用户名不能为空";
}
),
TextFormField(
controller: _pwdController,
decoration: InputDecoration(
labelText: "密码",
hintText: "您的登录密码",
icon: Icon(Icons.lock)
),
obscureText: true,
//校验密码
validator: (v) {
return v
.trim()
.length > 5 ? null : "密码不能少于6位";
}
),
// 登录按钮
Padding(
padding: const EdgeInsets.only(top: 28.0),
child: Row(
children: [
Expanded(
child: RaisedButton(
padding: EdgeInsets.all(15.0),
child: Text("登录"),
color: Theme
.of(context)
.primaryColor,
textColor: Colors.white,
onPressed: () {
//在这里不能通过此方式获取FormState,context不对
//print(Form.of(context));
// 通过_formKey.currentState 获取FormState后,
// 调用validate()方法校验用户名密码是否合法,校验
// 通过后再提交数据。
if((_formKey.currentState as FormState).validate()){
//验证通过提交数据
}
},
),
),
],
),
)
],
),
),
),
);
}
}
运行后效果如图3-29所示:
注意,登录按钮的onPressed
方法中不能通过Form.of(context)
来获取,原因是,此处的context
为FormTestRoute
的context,而Form.of(context)
是根据所指定context
向根去查找,而FormState
是在FormTestRoute
的子树中,所以不行。正确的做法是通过Builder
来构建登录按钮,Builder
会将widget
节点的context
作为回调参数:
Expanded(
// 通过Builder来获取RaisedButton所在widget树的真正context(Element)
child:Builder(builder: (context){
return RaisedButton(
...
onPressed: () {
//由于本widget也是Form的子代widget,所以可以通过下面方式获取FormState
if(Form.of(context).validate()){
//验证通过提交数据
}
},
);
})
)
其实context
正是操作Widget所对应的Element
的一个接口,由于Widget树对应的Element
都是不同的,所以context
也都是不同的,有关context
的更多内容会在后面高级部分详细讨论。Flutter中有很多“of(context)”这种方法,读者在使用时一定要注意context
是否正确。