mcpField
F
flutter-building-forms

flutter-building-forms

github.com/thesimpleapp开发052 次安装11 周前更新
代码仓库

关于 flutter-building-forms

面向 Flutter 开发者的表单构建技能,规范使用 Form、字段、验证、焦点、键盘和自动填充。强调遵循项目既有主题与组件,并覆盖提交、错误处理和验证检查。

主要功能

  1. 01规范 Form 与字段选择
  2. 02管理验证与服务端错误
  3. 03配置键盘、焦点与自动填充
  4. 04适配主题并预留错误空间
  5. 05提供检查与分析步骤

适用场景

  1. 01构建登录或注册表单
  2. 02实现需要验证的下拉选项
  3. 03处理服务器返回的字段错误
  4. 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

硬性规则

  1. 在 State 中持有 final GlobalKey<FormState> _formKey。绝不要在 build 中创建该键、FocusNode 或 TextEditingController。提交流程:if (_formKey.currentState!.validate()) { _formKey.currentState!.save(); … }。提交失败后,调用 validateGranularly(),然后对视觉顺序中第一个无效字段调用 FocusNode.requestFocus()(Set 无序)。
  2. 在 Form 内,所有需要验证或保存的值都必须使用 TextFormField、DropdownMenuFormField 或自定义的 FormField<T>——绝不要使用普通的 TextField。
  3. validator 的类型为 FormFieldValidator<T>?,返回 String?(null = 有效)。验证逻辑应保持轻量且同步。API / 服务器错误使用 forceErrorText。
  4. 默认的 autovalidateMode 为 AutovalidateMode.disabled。首次提交后优先使用 onUserInteraction 或 onUnfocus。必填且为空的字段不要使用 always。没有 AutovalidateMode.onFocusChange。还可使用:onUserInteractionIfError。
  5. 通过 ThemeData.inputDecorationTheme(InputDecorationThemeData)设置样式。每个字段的 InputDecoration 仅设置 labelText / hintText / helperText / prefixIcon。
  6. 为每个字段设置 keyboardType 和 textInputAction(最后一个字段之前设为 next,最后一个设为 done / go / send + onFieldSubmitted)。
  7. 设置 autofillHints: const [AutofillHints.…]。登录:username + password。注册:newUsername + newPassword。还可使用 email、oneTimeCode。将这些字段放在 AutofillGroup 中。成功提交后调用一次 TextInput.finishAutofillContext(shouldSave: true)——多步骤表单需等到最后一步之后再调用。
  8. 在 State 中仅创建一次控制器和 FocusNode,并在 dispose() 中释放它们。在惰性 ListView / CustomScrollView 中,控制器必须由有状态祖先持有。
  9. 预留辅助文本 / 错误文本的空间(helperText: ' ' 或固定高度的父组件),避免显示 errorText 时表单重新布局。
  10. 在 Form 中使用的新下拉菜单:DropdownMenuFormField。如果仍需处理 DropdownButtonFormField,请传入 initialValue,不要使用已弃用的 value。
  11. 第一个字段设置 autofocus。使用 FocusTraversalGroup 包裹表单。仅当视觉顺序 ≠ 阅读顺序时,才使用 OrderedTraversalPolicy + NumericFocusOrder。
  12. 可交互字段使用 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。
  • 让错误文本出现时没有预留辅助文本行。

验证

  1. 使用 grep 检查 Form 下的 TextField(、autovalidate:、AutovalidateMode.always、autofillHints: null、DropdownButtonFormField + value:,以及在 build 中创建的任何 GlobalKey / FocusNode / TextEditingController(。
  2. 每个需要验证的字段都是 FormField。每组登录/注册字段都位于同一个 AutofillGroup 中,且其子项一并构建。
  3. dart analyze / flutter analyze 检查无误。测试空值、无效值、服务器错误和键盘打开时的状态。

评论

支持 Markdown