Flutter TextFormField失焦时触发onFieldSubmitted问题咨询(Cubit+Stepper场景)
Flutter Cubit 动态Stepper表单字段更新失焦问题解决方案
根因修复:移除Stepper随机Key
你遇到的onChanged触发输入框失焦问题的核心原因是给Stepper设置了随机Key:每次Cubit状态更新触发页面重绘时,随机Key都会生成新值,导致整个Stepper组件完全销毁重建,所有子输入框的焦点、临时输入状态都会被清空。
而设置随机Key是为了规避Stepper动态修改步骤数量时抛出的widget.steps.length == oldWidget.steps.length错误,可采用以下更合理的方案替代随机Key:
- 给Stepper设置与步骤数量绑定的固定格式Key,示例:
Key('stepper_${currentStepCount}'),只有步骤数量真的发生变更时才会重建Stepper,普通字段状态更新不会触发Stepper重建,自然不会导致输入框失焦。 - 自行重写Stepper组件的长度校验逻辑,或使用支持动态步骤的第三方Stepper实现,从根源上解决动态步骤报错问题,无需额外设置特殊Key。
实现输入框失焦自动更新Cubit状态
修复Stepper重建问题后,可通过以下两种方案实现单字段编辑完成(失焦)即更新状态的需求,无需依赖onSaved或仅支持回车触发的onFieldSubmitted:
方案1:用FocusNode监听失焦事件
给每个TextFormField绑定独立的FocusNode,监听焦点变化事件,失焦时触发Cubit状态更新,示例代码如下:
class EditFormPage extends StatefulWidget { const EditFormPage({super.key}); @override State<EditFormPage> createState() => _EditFormPageState(); } class _EditFormPageState extends State<EditFormPage> { late final EditCubit cubit; // 为每个输入字段声明控制器和焦点节点 late final TextEditingController _nameController; late final FocusNode _nameFocusNode; late final TextEditingController _descController; late final FocusNode _descFocusNode; @override void initState() { super.initState(); cubit = context.read<EditCubit>(); // 初始化时用Cubit状态填充初始值 _nameController = TextEditingController(text: cubit.state.name); _nameFocusNode = FocusNode(); _descController = TextEditingController(text: cubit.state.desc); _descFocusNode = FocusNode(); // 绑定失焦监听 _nameFocusNode.addListener(() { if (!_nameFocusNode.hasFocus) { cubit.updateName(_nameController.text); } }); _descFocusNode.addListener(() { if (!_descFocusNode.hasFocus) { cubit.updateDesc(_descController.text); } }); } @override void dispose() { _nameController.dispose(); _nameFocusNode.dispose(); _descController.dispose(); _descFocusNode.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return Stepper( // 用步骤数量绑定的Key,替代随机Key key: Key('stepper_${context.watch<EditCubit>().state.stepCount}'), steps: [ Step( title: const Text('基础信息'), content: Column( children: [ TextFormField( controller: _nameController, focusNode: _nameFocusNode, decoration: const InputDecoration(labelText: '名称'), // 可保留onFieldSubmitted实现回车跳转下一个输入框 onFieldSubmitted: (_) => FocusScope.of(context).requestFocus(_descFocusNode), ), TextFormField( controller: _descController, focusNode: _descFocusNode, decoration: const InputDecoration(labelText: '描述'), ) ], ) ) // 其他步骤... ], ); } }
方案2:封装通用自动保存输入组件
如果表单字段较多,不想逐个维护控制器和焦点节点,可以封装通用的带失焦回调的输入组件,示例代码如下:
class AutoSaveTextField extends StatelessWidget { final String initialValue; final Function(String value) onSave; final InputDecoration? decoration; final TextInputAction? textInputAction; final void Function(String)? onFieldSubmitted; const AutoSaveTextField({ super.key, required this.initialValue, required this.onSave, this.decoration, this.textInputAction, this.onFieldSubmitted, }); @override Widget build(BuildContext context) { final controller = TextEditingController(text: initialValue); final focusNode = FocusNode(); focusNode.addListener(() { if (!focusNode.hasFocus) { onSave(controller.text); } }); return TextFormField( controller: controller, focusNode: focusNode, decoration: decoration, textInputAction: textInputAction, onFieldSubmitted: onFieldSubmitted, ); } }
使用时直接透传初始值和保存逻辑即可:
AutoSaveTextField( initialValue: cubit.state.name, onSave: (value) => cubit.updateName(value), decoration: const InputDecoration(labelText: '名称'), )
特殊场景兼容
如果因业务逻辑必须保留Stepper的随机Key,可给每个Step对应的内容组件的State混入AutomaticKeepAliveClientMixin,将wantKeepAlive返回值设为true,即可在Stepper重建时保留子组件的焦点和输入状态,避免失焦问题。
内容的提问来源于stack exchange,提问作者Henrique Melo
相关产品推荐
相关产品推荐

