Files
App.K12.Student.Miniapp_Flu…/docs/demos/form_demo.md
T
2026-09-04 15:22:20 +08:00

187 lines
6.2 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 【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<FormState> _formKey = GlobalKey<FormState>(); // 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\<bool> 校验"非输入型"字段
```dart
FormField<bool>(
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 分层思想)。