flutter架 构项目基础
This commit is contained in:
@@ -0,0 +1,186 @@
|
||||
# 【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 分层思想)。
|
||||
Reference in New Issue
Block a user