关于 flutter-building-forms
面向 Flutter 开发者的表单构建技能,规范使用 Form、字段、验证、焦点、键盘和自动填充。强调遵循项目既有主题与组件,并覆盖提交、错误处理和验证检查。
主要功能
- 01规范 Form 与字段选择
- 02管理验证与服务端错误
- 03配置键盘、焦点与自动填充
- 04适配主题并预留错误空间
- 05提供检查与分析步骤
适用场景
- 01构建登录或注册表单
- 02实现需要验证的下拉选项
- 03处理服务器返回的字段错误
- 04审查表单的键盘与焦点行为
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ thesimpleapp/ flutter-app-skills --skill flutter-building-forms安装后在 Claude Code 中输入/flutter-building-forms确认已加载。
附带文件references/recipes.md
详细介绍
Flutter 表单构建
在 Flutter 3.47+ 中使用 Form / FormField 提交表单。不要添加 GetX、flutter_form_builder 或 Expo/RN 表单模式。如果此仓库已有 Form 封装、共享验证器、ThemeData.inputDecorationTheme,或在身份验证区域使用了 AutofillGroup,请在此基础上扩展。如果组件已读取项目的 ThemeExtension 或 *Pressable,请使用该系统为字段设置样式——不要在品牌化界面上套用默认的 Material 装饰主题。
使用项目现有的 ListView / CustomScrollView 滚动表单。键盘下方直接放置 Column 会导致溢出。
参考资料
references/
recipes.md Login/signup autofill, server errors, custom FormField, lazy lists
硬性规则
- 在
State中持有final GlobalKey<FormState> _formKey。绝不要在build中创建该键、FocusNode或TextEditingController。提交流程:if (_formKey.currentState!.validate()) { _formKey.currentState!.save(); … }。提交失败后,调用validateGranularly(),然后对视觉顺序中第一个无效字段调用FocusNode.requestFocus()(Set无序)。 - 在
Form内,所有需要验证或保存的值都必须使用TextFormField、DropdownMenuFormField或自定义的FormField<T>——绝不要使用普通的TextField。 validator的类型为FormFieldValidator<T>?,返回String?(null= 有效)。验证逻辑应保持轻量且同步。API / 服务器错误使用forceErrorText。- 默认的
autovalidateMode为AutovalidateMode.disabled。首次提交后优先使用onUserInteraction或onUnfocus。必填且为空的字段不要使用always。没有AutovalidateMode.onFocusChange。还可使用:onUserInteractionIfError。 - 通过
ThemeData.inputDecorationTheme(InputDecorationThemeData)设置样式。每个字段的InputDecoration仅设置labelText/hintText/helperText/prefixIcon。 - 为每个字段设置
keyboardType和textInputAction(最后一个字段之前设为next,最后一个设为done/go/send+onFieldSubmitted)。 - 设置
autofillHints: const [AutofillHints.…]。登录:username+password。注册:newUsername+newPassword。还可使用email、oneTimeCode。将这些字段放在AutofillGroup中。成功提交后调用一次TextInput.finishAutofillContext(shouldSave: true)——多步骤表单需等到最后一步之后再调用。 - 在
State中仅创建一次控制器和FocusNode,并在dispose()中释放它们。在惰性ListView/CustomScrollView中,控制器必须由有状态祖先持有。 - 预留辅助文本 / 错误文本的空间(
helperText: ' '或固定高度的父组件),避免显示errorText时表单重新布局。 - 在 Form 中使用的新下拉菜单:
DropdownMenuFormField。如果仍需处理DropdownButtonFormField,请传入initialValue,不要使用已弃用的value。 - 第一个字段设置
autofocus。使用FocusTraversalGroup包裹表单。仅当视觉顺序 ≠ 阅读顺序时,才使用OrderedTraversalPolicy+NumericFocusOrder。 - 可交互字段使用
const ValueKey<String>('kebab-name')。
持有 Form
class _SignInState extends State<SignInPage> {
final GlobalKey<FormState> _formKey = GlobalKey<FormState>();
late final FocusNode _emailFocus = FocusNode();
late final TextEditingController _email = TextEditingController();
AutovalidateMode _auto = AutovalidateMode.disabled;
@override
void dispose() {
_emailFocus.dispose();
_email.dispose();
super.dispose();
}
void _submit() {
final Set<FormFieldState<Object?>> invalid =
_formKey.currentState!.validateGranularly();
if (invalid.isNotEmpty) {
setState(() => _auto = AutovalidateMode.onUserInteraction);
_emailFocus.requestFocus(); // first invalid field in visual order
return;
}
_formKey.currentState!.save();
TextInput.finishAutofillContext(shouldSave: true);
}
}
嵌套组件可以调用 Form.of(context)(缺少 Form 时会触发断言)或 Form.maybeOf,而不是逐层传递键。验证表单。
validate() 会运行所有后代字段的 validator,重建界面以显示错误,并在有任一项验证失败时返回 false。save() 只调用 onSaved——不会进行验证。如果使用 onSaved,请在 validate() 成功后调用 save()。reset() 会将每个字段还原为 FormField.initialValue。validateGranularly() 会返回无效 FormFieldState 的 Set。FormState.validate。
字段
TextFormField 是一个包装了 TextField 的 FormField<String>。Form 祖先节点是可选的;若没有,请传入 GlobalKey<FormFieldState>,并在该状态上调用 validate / save / reset。只有当编辑器不应参与 FormState 时,才使用 TextField。如果传入 TextEditingController,其 .text 就是该字段的 initialValue。FormField.initialValue 会在首次构建时生效,并在调用 reset() 时再次生效。TextFormField。
需要调用 validate / save / reset 的下拉菜单:
DropdownMenuFormField<String>(
key: const ValueKey<String>('country-field'),
dropdownMenuEntries: const <DropdownMenuEntry<String>>[
DropdownMenuEntry<String>(value: 'us', label: 'United States'),
],
validator: (String? v) => v == null ? 'Pick a country' : null,
onSaved: (String? v) { /* store */ },
)
使用 initialSelection 设置初始选中项(映射到 FormField.initialValue)。原始的 DropdownMenu 不是 FormField。DropdownMenuFormField。
自定义复选框组、滑块、芯片选择器:使用 FormField<T>,让其 builder 读取 FormFieldState 并调用 state.didChange(newValue)。FormField。
验证
TextFormField(
validator: (String? v) {
if (v == null || v.isEmpty) return 'Required';
return null;
},
forceErrorText: serverEmailError, // null resumes validator
autovalidateMode: AutovalidateMode.onUserInteractionIfError,
decoration: const InputDecoration(
labelText: 'Email',
helperText: ' ',
),
)
forceErrorText 会写入 FormFieldState.errorText,而不会运行 validator。只要它非空,就不会调用 validator,并且它会覆盖 InputDecoration.errorText。清除它即可恢复本地验证。forceErrorText。
使用 autovalidateMode,不要使用已移除的 autovalidate 布尔值。AutovalidateMode:
| 值 | 运行时机 |
|---|---|
disabled | 仅在调用 validate / validateGranularly 时(Form/FormField 的默认值) |
always | 首次构建时以及每次更改时(空的必填字段会在绘制时立刻报错) |
onUserInteraction | 字段内容更改后 |
onUnfocus | 字段失去焦点后 |
onUserInteractionIfError | 字段已有错误后,每次更改时 |
当 errorText 非空时,辅助文本会被隐藏,字段也可能增高。使用 helperText: ' ' 或固定高度的父级预留空间。
键盘、操作、自动填充
将 iOS 提示与键盘类型匹配:AutofillHints.email + TextInputType.emailAddress、AutofillHints.name + TextInputType.name。TextInputType。
TextInputAction.next 会将焦点移至同一个 FocusScope 中下一个可聚焦的元素。中间字段使用 next。最后一个字段使用 done(或 go / search / send),并在 onFieldSubmitted 中处理提交。不要在 iOS 上使用 previous 或 none。TextInputAction。
传入 autofillHints: const [AutofillHints.email]——除非根据 defaultTargetPlatform 分支处理,否则不要使用临时字符串。不要传入 null(这会让该字段在 Android/web 上退出自动填充)。iOS 和 web 只使用第一个提示;Android 使用整个列表。
将一个登录 / 注册 / 结账字段组放入 AutofillGroup 中。客户端必须一起构建——尚未布局的 ListView 子项不会被注册。成功提交后,在每个屏幕上调用一次 TextInput.finishAutofillContext(shouldSave: true),并且要在最后一步之后调用。AutofillGroup。
焦点
在 initState 中创建 FocusNode,或将其设为只初始化一次的 late 字段。在第一个字段上设置 autofocus: true。验证失败后,调用 myFocusNode.requestFocus()——不要调用 FocusScope.of(context).requestFocus(focusNode)。Focus cookbook。
默认的 Tab / 阅读顺序为 ReadingOrderTraversalPolicy。将表单包装在 FocusTraversalGroup 中。当视觉顺序 ≠ 遍历顺序时:
FocusTraversalGroup(
policy: OrderedTraversalPolicy(),
child: FocusTraversalOrder(
order: const NumericFocusOrder(1),
child: emailField,
),
)
在同一个组中混用不可比较的 FocusOrder 类型会触发断言。Focus。
主题
ThemeData(
inputDecorationTheme: const InputDecorationThemeData(
filled: true,
border: OutlineInputBorder(),
contentPadding: EdgeInsets.symmetric(horizontal: 16, vertical: 12),
floatingLabelBehavior: FloatingLabelBehavior.auto,
errorMaxLines: 2,
),
)
InputDecoration.applyDefaults 只会从此主题填充装饰中为 null 的字段。
不要
- 在
build中分配GlobalKey<FormState>、FocusNode或TextEditingController。 - 在
Form下放置裸TextField,并期待FormState.validate()能检查它。 - 使用已弃用的
autovalidate: true,臆造AutovalidateMode.onFocusChange,或设置always,导致必填项在首次绘制时就报错。 - 使用
InputDecoration.errorText显示错误,同时又使用validator。 - 省略
autofillHints、将其设为null,或在创建账号字段(newPassword)上使用AutofillHints.password。 - 传入
DropdownButtonFormField.value而不是initialValue,或在表单需要通过validate()验证所选项时使用原始的DropdownMenu。 - 将
TextFormField放入延迟加载的ListView,却没有使用由长生命周期祖先拥有的controller。 - 在每个字段上复制粘贴完整的
InputDecoration边框、颜色和内边距。 - 不对控制器和焦点节点调用
dispose(),或在成功提交凭据后不调用TextInput.finishAutofillContext。 - 让错误文本出现时没有预留辅助文本行。
验证
- 使用
grep检查Form下的TextField(、autovalidate:、AutovalidateMode.always、autofillHints: null、DropdownButtonFormField+value:,以及在build中创建的任何GlobalKey/FocusNode/TextEditingController(。 - 每个需要验证的字段都是
FormField。每组登录/注册字段都位于同一个AutofillGroup中,且其子项一并构建。 dart analyze/flutter analyze检查无误。测试空值、无效值、服务器错误和键盘打开时的状态。