You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

扩展FormField自定义表单字段时键盘弹出后立即关闭问题排查

问题分析与解决方案

首先,这不是Flutter的Bug,核心问题出在你对TextFormField的状态同步方式和Key的使用逻辑上,咱们一步步拆解:

为什么会出现这些矛盾现象?

1. 不设置Key时,按钮点击无法更新内容

TextFormField的initialValue只在组件第一次初始化时生效。当你点击加减按钮调用state.didChange()更新FormField的状态后,TextFormField的widget并没有被Flutter标记为需要重新构建(因为没有唯一标识,Flutter会复用旧的widget实例),所以它不会读取新的state.value来更新显示内容。

2. 设置UniqueKey时键盘弹出后立即关闭

UniqueKey会在每次组件重建时生成一个全新的key,这会让Flutter认为这是一个全新的TextFormField组件,于是会销毁旧的实例并创建新的。旧实例的焦点丢失,自然导致键盘关闭。

3. iOS物理设备的报错和当前问题无关

你看到的RBSServiceErrorDomain报错是Gboard键盘扩展的权限问题,和Flutter的表单逻辑没有关系,可以忽略它。

正确的解决方案:用TextEditingController同步状态

不要依赖initialValue来同步FormField的状态,而是使用TextEditingController来直接控制TextFormField的内容,同时双向同步用户输入和FormField的状态。

修改后的代码如下:

class CustomNumberFormField extends FormField<double> {
  final int scale;
  final TextStyle? textStyle;

  CustomNumberFormField({
    required FormFieldSetter<double> onSaved,
    this.scale = 0,
    this.textStyle,
    super.validator,
    super.key,
    super.initialValue,
  }) : super(
          onSaved: onSaved,
          builder: (FormFieldState<double> state) {
            // 获取自定义state中的controller
            late final TextEditingController controller;
            if (state is _CustomNumberFormFieldState) {
              controller = state.controller;
            } else {
              controller = TextEditingController(
                text: NumberFormat("#.#").format(initialValue),
              );
            }

            return Column(
              mainAxisAlignment: MainAxisAlignment.center,
              crossAxisAlignment: CrossAxisAlignment.end,
              children: [
                Row(
                  mainAxisSize: MainAxisSize.min,
                  children: <Widget>[
                    IconButton(
                      icon: const Icon(Icons.remove),
                      onPressed: () {
                        final newValue = (state.value != null)
                            ? state.value! - 1
                            : (initialValue ?? 0);
                        state.didChange(newValue);
                        // 直接更新输入框内容
                        controller.text = NumberFormat("#.#").format(newValue);
                      },
                    ),
                    SizedBox(
                      width: 100,
                      child: TextFormField(
                        // 不再需要UniqueKey,用controller控制内容
                        controller: controller,
                        keyboardType: TextInputType.numberWithOptions(
                            decimal: (scale > 0)),
                        textAlign: TextAlign.center,
                        inputFormatters: <TextInputFormatter>[
                          FilteringTextInputFormatter.allow(RegExp(
                              scale > 0 ? r'[0-9]+[,.]{0,1}[0-9]*' : r'[0-9]')),
                          TextInputFormatter.withFunction(
                            (oldValue, newValue) => newValue.copyWith(
                              text: newValue.text.replaceAll('.', ','),
                            ),
                          ),
                        ],
                        style: textStyle,
                        autovalidateMode: AutovalidateMode.onUserInteraction,
                        onChanged: (value) {
                          // 同步用户输入到FormField状态
                          if (value.isNotEmpty) {
                            try {
                              final number = NumberFormat().parse(value).toDouble();
                              state.didChange(number);
                            } catch (_) {
                              // 处理无效输入的情况
                            }
                          } else {
                            state.didChange(null);
                          }
                        },
                        onSaved: (value) {
                          if (value != null && value.isNotEmpty) {
                            onSaved(NumberFormat().parse(value).toDouble());
                          }
                        },
                      ),
                    ),
                    IconButton(
                      icon: const Icon(Icons.add),
                      onPressed: () {
                        final newValue = (state.value != null)
                            ? state.value! + 1
                            : (initialValue ?? 0);
                        state.didChange(newValue);
                        // 直接更新输入框内容
                        controller.text = NumberFormat("#.#").format(newValue);
                      },
                    ),
                  ],
                ),
                state.hasError
                    ? Text(
                        state.errorText ?? "",
                        style: const TextStyle(color: Colors.red),
                      )
                    : Container()
              ],
            );
          },
        );

  @override
  FormFieldState<double> createState() => _CustomNumberFormFieldState();
}

class _CustomNumberFormFieldState extends FormFieldState<double> {
  late final TextEditingController controller;

  @override
  void initState() {
    super.initState();
    // 初始化controller,绑定初始值
    controller = TextEditingController(
      text: NumberFormat("#.#").format(initialValue),
    );
  }

  @override
  void didUpdateWidget(covariant CustomNumberFormField oldWidget) {
    super.didUpdateWidget(oldWidget);
    // 如果外部更新了初始值,同步到输入框
    if (oldWidget.initialValue != widget.initialValue) {
      controller.text = NumberFormat("#.#").format(widget.initialValue);
    }
  }

  @override
  void dispose() {
    controller.dispose();
    super.dispose();
  }
}

关键改动说明

  1. 新增自定义FormFieldState:用来持有TextEditingController,避免每次builder重建时创建新的controller,同时妥善处理controller的生命周期(初始化、更新、销毁)。
  2. 双向状态同步:
    • 点击加减按钮时,更新FormField的状态,同时直接修改controller.text来更新输入框内容。
    • 用户输入时,通过onChanged回调将输入内容解析为数字,同步到FormField的状态。
  3. 移除UniqueKey:不再需要通过key强制重建组件,controller的更新会直接触发输入框内容变化,同时保持焦点和键盘状态。

这样修改后,既能实现按钮点击更新输入框内容,又能保证键盘正常工作,解决了你遇到的所有矛盾问题。

内容的提问来源于stack exchange,提问作者Taco

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.04 16:56:14