扩展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(); } }
关键改动说明
- 新增自定义
FormFieldState:用来持有TextEditingController,避免每次builder重建时创建新的controller,同时妥善处理controller的生命周期(初始化、更新、销毁)。 - 双向状态同步:
- 点击加减按钮时,更新
FormField的状态,同时直接修改controller.text来更新输入框内容。 - 用户输入时,通过
onChanged回调将输入内容解析为数字,同步到FormField的状态。
- 点击加减按钮时,更新
- 移除
UniqueKey:不再需要通过key强制重建组件,controller的更新会直接触发输入框内容变化,同时保持焦点和键盘状态。
这样修改后,既能实现按钮点击更新输入框内容,又能保证键盘正常工作,解决了你遇到的所有矛盾问题。
内容的提问来源于stack exchange,提问作者Taco
相关产品推荐
相关产品推荐

