flutter架 构项目基础

This commit is contained in:
崔宝新
2026-09-04 15:22:20 +08:00
parent 70967229d0
commit 0c5aee59c0
265 changed files with 35484 additions and 0 deletions
+186
View File
@@ -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 分层思想)。