1、Flutter表单基础:认识Form与FormField,构建你的第一个表单页面
表单开发,说白了就是跟用户打交道最频繁的那部分代码。登录、注册、个人信息编辑……哪个App都绕不开。我刚接触Flutter时,觉得表单不就是几个输入框加个按钮吗?后来踩了不少坑才明白——表单的难点从来不在UI,而在状态管理和校验逻辑。
这一章,我们先从最基础的东西聊起。把Form和FormField这两个核心角色搞清楚,然后手把手搭出第一个表单页面。
1.1 Form与FormField:表单里的「指挥官」和「士兵」
Flutter里的表单体系,其实就两个关键角色:
- Form:整个表单的容器,负责统筹全局。比如一键校验所有字段、一键重置数据。
- FormField:表单里的每个具体字段。比如用户名输入框、密码输入框、下拉选择器。
你可以把Form想象成一个指挥官,FormField就是它手下的士兵。指挥官不关心士兵具体怎么打仗,但士兵打完仗要向指挥官汇报结果。
我个人习惯用GlobalKey<FormState>来关联Form,这样就能在任意地方调用formKey.currentState!.validate()来触发校验。嗯,这里要注意——currentState可能为null,所以加个!或者判空会更安全。
1.2 最简单的FormField:TextFormField
实际开发中,TextFormField是我们用得最多的FormField子类。它本质上就是TextField的增强版,自带校验和状态管理能力。
来看一个最基础的例子:
Form(
key: _formKey,
child: Column(
children: [
TextFormField(
decoration: InputDecoration(labelText: '用户名'),
validator: (value) {
if (value == null || value.isEmpty) {
return '用户名不能为空';
}
return null;
},
),
SizedBox(height: 16),
ElevatedButton(
onPressed: () {
if (_formKey.currentState!.validate()) {
// 校验通过,处理数据
}
},
child: Text('提交'),
),
],
),
)
这段代码里,validator函数返回null表示校验通过,返回字符串表示错误提示。这个设计我觉得挺巧妙的——用返回值类型来区分「通过」和「不通过」,比用布尔值加错误消息两个参数要简洁。
autovalidateMode: AutovalidateMode.onUserInteraction,只在用户交互后才校验,体验好很多。
1.3 自定义FormField:当内置组件不够用时
TextFormField虽然好用,但总有不够用的时候。比如你要做一个「评分组件」或者「颜色选择器」,这些没有现成的FormField子类。
这时候就需要自己动手了。Flutter提供了FormField<T>这个泛型类,你可以自定义任何类型的表单字段。
举个例子,一个简单的「是否同意协议」的复选框字段:
FormField<bool>(
initialValue: false,
validator: (value) {
if (value != true) {
return '请先同意用户协议';
}
return null;
},
builder: (FormFieldState<bool> state) {
return Column(
children: [
Checkbox(
value: state.value,
onChanged: (value) {
state.didChange(value);
},
),
if (state.hasError)
Text(
state.errorText!,
style: TextStyle(color: Colors.red),
),
],
);
},
)
关键点在于state.didChange(value)——你必须手动调用这个方法,告诉Form「这个字段的值变了」。否则Form根本不知道你的字段状态,校验也就无从谈起。
didChange,结果校验永远拿不到最新值。排查了半天才发现是这个问题。记住:任何自定义字段,值变化后必须调用state.didChange()。
1.4 构建你的第一个表单页面
理论说完了,我们来实战。下面是一个完整的登录表单页面,包含了用户名、密码、记住我复选框:
class LoginFormPage extends StatefulWidget {
@override
_LoginFormPageState createState() => _LoginFormPageState();
}
class _LoginFormPageState extends State<LoginFormPage> {
final _formKey = GlobalKey<FormState>();
final _usernameController = TextEditingController();
final _passwordController = TextEditingController();
bool _rememberMe = false;
@override
void dispose() {
_usernameController.dispose();
_passwordController.dispose();
super.dispose();
}
void _handleSubmit() {
if (_formKey.currentState!.validate()) {
// 这里可以拿到所有字段的值
final username = _usernameController.text;
final password = _passwordController.text;
print('登录信息:$username, $password, 记住我:$_rememberMe');
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('登录')),
body: Padding(
padding: EdgeInsets.all(16),
child: Form(
key: _formKey,
child: Column(
children: [
TextFormField(
controller: _usernameController,
decoration: InputDecoration(labelText: '用户名'),
validator: (v) => v!.isEmpty ? '请输入用户名' : null,
),
SizedBox(height: 12),
TextFormField(
controller: _passwordController,
obscureText: true,
decoration: InputDecoration(labelText: '密码'),
validator: (v) => v!.length < 6 ? '密码至少6位' : null,
),
FormField<bool>(
initialValue: false,
builder: (state) {
return CheckboxListTile(
title: Text('记住我'),
value: state.value,
onChanged: (v) {
state.didChange(v);
setState(() => _rememberMe = v!);
},
);
},
),
SizedBox(height: 24),
ElevatedButton(
onPressed: _handleSubmit,
child: Text('登录'),
),
],
),
),
),
);
}
}
这个例子涵盖了本章所有知识点:
- 用
GlobalKey管理Form状态 - 用
TextFormField处理文本输入 - 用自定义
FormField<bool>处理复选框 - 用
validator做字段校验
1.5 本章小结
这一章我们认识了Form和FormField这对黄金搭档。Form负责统筹,FormField负责具体字段。内置的TextFormField能满足大部分场景,不够用时就用自定义FormField。
你想想看,表单开发其实没那么神秘。无非就是「收集数据 → 校验数据 → 提交数据」这个流程。Flutter把最繁琐的状态管理部分封装好了,我们只需要关注业务逻辑就行。
下一章,我会聊聊表单校验的进阶技巧——正则表达式、异步校验、联动校验。这些在实际项目中非常实用,到时候我会分享一些我踩过的坑和总结的经验。