# 【Demo】Form 表单校验(7 个知识点) > 源文件:`lib/pages/demo/form_demo.dart` | 分类:基础布局 > 一句话:注册表单校验的完整范式 —— validator、时机、跨字段对比、协议勾选、两段式校验。 --- ## 一、这个 Demo 解决什么问题 做注册/登录表单时新手最头疼的几件事,本页一次讲全: 必填/长度/正则/密码强度校验、确认密码对比、协议勾选校验、 **validator 不能 await** 时怎么做异步查重、防重复提交、重置表单。 ### 和 Vue 的对照(关键差异) | Vue | Flutter | |---|---| | v-model 双向绑定 + rules 规则对象 | TextEditingController + 每个字段自带 `validator` 函数 | | `validate()`(async-validator) | `formKey.currentState.validate()` 统一触发 | **Flutter 的 validator 是同步函数,返回 `String?`**: - 返回 `null` → 校验通过; - 返回字符串 → 错误文本(自动显示在输入框下方红字)。 没有"规则对象",每个字段的规则就写在它自己的 validator 里,**就近维护**。 --- ## 二、怎么运行 / 演示内容 ```dart runApp(const MaterialApp(home: FormDemoPage())); ``` 5 个输入框(用户名/手机号/邮箱/密码/确认密码)+ 协议勾选 + 提交/重置: - 不填就提交 → 红字提示;填错即时提示(onUserInteraction); - 提交通过 → 模拟 1 秒异步查重 → "注册成功"; - 重置 → 清空所有输入和校验状态。 --- ## 三、核心知识点(新手向) ### 1. Form 的骨架 ```dart final GlobalKey _formKey = GlobalKey(); // Form 的唯一钥匙 Form( key: _formKey, autovalidateMode: AutovalidateMode.onUserInteraction, // 校验时机 child: Column(children: [ TextFormField(validator: ...), ... ]), ) // 提交 / 重置都靠这把钥匙 _formKey.currentState!.validate(); _formKey.currentState?.reset(); ``` ### 2. 三种校验时机(AutovalidateMode) | 模式 | 行为 | 建议 | |---|---|---| | `disabled`(默认) | 只在点提交 validate() 时校验 | 简单表单 | | `always` | build 时立刻校验 | **慎用**:空表单满屏红字 | | `onUserInteraction` | 用户碰过的字段才即时校验 | **推荐** | ### 3. validator 写法范式 ```dart // 必填 + 长度 String? _validateName(String? v) { final s = v?.trim() ?? ''; if (s.isEmpty) return '请输入用户名'; if (s.length < 2 || s.length > 8) return '用户名需 2-8 个字符'; return null; } // 正则全匹配:^...$ 必须写!不然 11 位之后还能续数字 String? _validatePhone(String? v) { final s = v?.trim() ?? ''; if (s.isEmpty) return '请输入手机号'; if (!RegExp(r'^1[3-9]\d{9}$').hasMatch(s)) return '手机号格式不正确'; return null; } // 跨字段对比:validator 里读另一个 controller String? _validatePwd2(String? v) { if (v == null || v.isEmpty) return '请再次输入密码'; if (v != _pwdC.text) return '两次密码不一致'; return null; } ``` ### 4. 两段式校验(重点!validator 不能 await) ```dart // ① 同步校验:所有 validator 逐个执行 if (!_formKey.currentState!.validate()) { _toast('有字段未通过校验,请检查红字提示'); return; } // ② 同步通过 → 再单独做异步业务校验(查重/验证码) setState(() => _submitting = true); await Future.delayed(const Duration(seconds: 1)); // 模拟查重请求 if (!mounted) return; // 异步后必须判 mounted 才能用 context ``` ### 5. 协议勾选:FormField\ 校验"非输入型"字段 ```dart FormField( initialValue: _agreed, validator: (v) => (v ?? false) ? null : '请先勾选同意协议', builder: (field) { return Column(children: [ InkWell( onTap: () { field.didChange(!(field.value ?? false)); // 更新 FormField 自身 setState(() => _agreed = field.value ?? false); // 同步外部镜像 }, child: ..., ), // 注意:自定义 FormField 的 errorText 要自己在 builder 里渲染! if (field.hasError) Text(field.errorText!, style: const TextStyle(color: Colors.red)), ]); }, ) ``` ### 6. 防重复提交 + 重置 ```dart // 提交中按钮禁用(onPressed: null 自动置灰 + 换 loading 转圈) FilledButton( onPressed: _submitting ? null : _submit, child: _submitting ? CircularProgressIndicator() : const Text('提交'), ) // 重置:reset() 只清校验错误和 FormField 状态,不碰 controller.text! void _reset() { _formKey.currentState?.reset(); _nameC.clear(); // 清空输入要手动 clear() ... } ``` --- ## 四、关键代码剖析 通用输入框抽取(本页把 label/hint/图标/校验规则全部参数化,避免 5 个输入框复制粘贴): ```dart Widget _buildField({ required String label, required TextEditingController controller, required String? Function(String?) validator, TextInputType? keyboardType, }) { return TextFormField( controller: controller, keyboardType: keyboardType, validator: validator, decoration: InputDecoration( labelText: label, prefixIcon: Icon(icon), border: const OutlineInputBorder(), ), ); } ``` --- ## 五、踩坑记录 1. **`formState.reset()` 不碰 `controller.text`** —— 清空输入必须手动 `clear()`; 2. **validator 是同步函数,不能 await** —— 异步校验(查重/验证码)放 validate() 通过之后单独做; 3. **正则要 `^...$` 全匹配**,否则 `RegExp(r'1[3-9]\d{9}')` 能匹配"123456789012345"; 4. 所有 controller 都要 dispose(本页 5 个); 5. 加了 `.w/.h/.sp` 的 widget 不能 `const`; 6. 异步(await)之后用 context 必须先 `if (!mounted) return`。 --- ## 六、扩展思考 - 校验规则多了以后,可以把 validator 抽成独立函数文件(如 `lib/core/validators/`),表单只负责组装; - 真实注册流程:本页"模拟查重"处换成 `DioClient` 请求(见 [dio_demo.md](dio_demo.md)); - 密码强度可以升级为正则组合:`(?=.*[A-Za-z])(?=.*\d)` 就是"必须同时含字母和数字"的零宽断言写法; - 表单 + 状态管理:把 controller 和提交逻辑挪进 Riverpod `NotifierProvider`,页面只剩纯 UI(见架构文档的 ExamPage 分层思想)。