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

6.2 KiB
Raw Blame History

【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 里,就近维护。


二、怎么运行 / 演示内容

runApp(const MaterialApp(home: FormDemoPage()));

5 个输入框(用户名/手机号/邮箱/密码/确认密码)+ 协议勾选 + 提交/重置:

  • 不填就提交 → 红字提示;填错即时提示(onUserInteraction);
  • 提交通过 → 模拟 1 秒异步查重 → "注册成功";
  • 重置 → 清空所有输入和校验状态。

三、核心知识点(新手向)

1. Form 的骨架

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 写法范式

// 必填 + 长度
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)

// ① 同步校验:所有 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> 校验"非输入型"字段

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. 防重复提交 + 重置

// 提交中按钮禁用(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 个输入框复制粘贴):

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);
  • 密码强度可以升级为正则组合:(?=.*[A-Za-z])(?=.*\d) 就是"必须同时含字母和数字"的零宽断言写法;
  • 表单 + 状态管理:把 controller 和提交逻辑挪进 Riverpod NotifierProvider,页面只剩纯 UI(见架构文档的 ExamPage 分层思想)。