前言
本篇文章将记录Form 表单的使用,在文末还会记录一个特别实用的
自动移动聚焦点 的方法。
一、表单Form
在实际业务开发中,在正式像服务器提交数据之前,都会对各个输入框数据进行合法性校验,但是对每一个TextField 都进行校验是一件很麻烦的事。如果用户想清除一组TextField 的数据,又非常的麻烦。flutter还给我们提供了一个From 组件,它可以对输入框进行分组,然后进行统一操作,如内容校验,输入框重置,输入内容保存等。
Form继承自StatefulWidget对象,它对应的状态类为FormState。我们先看看Form类的定义:
Form({
required Widget child,
bool autovalidate = false,
WillPopCallback onWillPop,
VoidCallback onChanged,
})
autovalidate : 是否自动校验输入内容,当为true 时,每个子FormFeild 内容发生变化时,都会自动校验合法性,并直接显示错误信息。否则,需要通过调用FormState.validate() 来手动校验。
onWillPop: 决定Form 所在的路由是否可以直接返回(如点击返回按钮),该回调返回一个Future对象,如果 Future 的最终结果是false,则当前路由不会返回;如果为true,则会返回到上一个路由。此属性通常用于拦截返回按钮。
onChanged:Form的任意一个子FormField内容发生变化时会触发此回调。
Form的子孙元素必须是FormField类型,FormField是一个抽象类,定义几个属性,FormState内部通过它们来完成操作,FormField部分定义如下:
const FormField({
...
FormFieldSetter<T> onSaved, //保存回调
FormFieldValidator<T> validator, //验证回调
T initialValue, //初始值
bool autovalidate = false, //是否自动校验。
})
为了方便使用,Flutter 提供了一个TextFormField组件,它继承自FormField类,也是TextField的一个包装类,所以除了FormField定义的属性之外,它还包括TextField的属性。
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 _MyHomePageState extends State<MyHomePage> {
TextEditingController userController = TextEditingController();
TextEditingController passController = TextEditingController();
GlobalKey _globalKey = GlobalKey<FormState>();
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Form(
key: _globalKey, //设置globalKey,用于后面获取FormState
autovalidateMode: AutovalidateMode.onUserInteraction,
child: Column(
children: [
TextFormField(
autofocus: true,
controller: userController,
decoration: InputDecoration(
labelText: "用户名",
hintText: "用户名或邮箱",
icon: Icon(Icons.person),
),
validator: (v) {
return v!.trim().isNotEmpty ? null : '用户名不能为空';
},
),
SizedBox(
height: 20,
),
TextFormField(
controller: passController,
decoration: InputDecoration(
labelText: "密码",
hintText: "输入密码",
icon: Icon(Icons.lock),
),
obscureText: true,
validator: (v) {
return v!.trim().length > 5 ? null : '密码不能少于6位';
},
),
Padding(
padding: EdgeInsets.only(top: 20),
child: Expanded(
child: ElevatedButton(
child: Padding(
padding: EdgeInsets.all(16),
child: Text("登录"),
),
onPressed: () {
// 通过_formKey.currentState 获取FormState后,
// 调用validate()方法校验用户名密码是否合法,校验
// 通过后再提交数据。
if((_globalKey.currentState as FormState).validate()){
print("验证通过,开始提交");
}
},
),
),
),
],
),
),
);
}
}
运行效果如下
注意,登录按钮的onPressed方法中不能通过Form.of(context)来获取,原因是,此处的context为FormTestRoute的context,而Form.of(context)是根据所指定context向根去查找,而FormState是在FormTestRoute的子树中,所以不行。正确的做法是通过Builder来构建登录按钮,Builder会将widget节点的context作为回调参数:
Expanded(
// 通过Builder来获取ElevatedButton所在widget树的真正context(Element)
child:Builder(builder: (context){
return ElevatedButton(
...
onPressed: () {
//由于本widget也是Form的子代widget,所以可以通过下面方式获取FormState
if(Form.of(context).validate()){
//验证通过提交数据
}
},
);
})
)
二、实用FocusScope 自动聚焦下一个
使用FocusScopeNode,移动聚焦点。
- 初始化FocusScopeNode 对象
- 初始化GlobalKey
FocusScopeNode scopeNode = FocusScopeNode();
GlobalKey<FormState> globalKey = GlobalKey<FormState>();
- 在代码中使用form表单
Form(
key: model.globalKey,
child: FocusScope(
node: model.scopeNode,
child: Column(
children: [
TextFormField(onEditingComplete: model.scopeNode.nextFocus,),
TextFormField(
onEditingComplete: model.scopeNode.nextFocus,
),
],
),
),
),
看到上面的代码,需要注意的onEditingComplete 事件,model.scopeNode.nextFocus 输出完成,点击done 会自动跳转下一个TextFormField。