Flutter提交或切换焦点时键盘类型短暂重置为默认问题求助
Flutter键盘类型在关闭/切换焦点时短暂重置为默认类型
在使用自定义AppInputField组件时,触发键盘的onSubmit关闭操作,或者切换输入框焦点的瞬间,设置好的键盘类型(比如TextInputType.phone)会短暂变回默认的文本键盘,再完成关闭/切换动作。
使用代码:
AppInputField( key: ValueKey(1), label: context.localizations.phone, placeholder: context.localizations.phonePlaceholderLogin, controller: _emailController, isPhoneField: true, keyboardType: TextInputType.phone, countryCodePicker: CountryCodePicker( countryCodes: _countries, onChanged: (value) { setState(() { _currentCountry = value; }); }, selectedCountry: _currentCountry, ), onChanged: (phone) { context.read<LoginBloc>().add(LoginEvent.phoneChanged(phone)); }, ),
自定义AppInputField代码:
class AppInputField extends StatelessWidget { final String label; final String placeholder; final String helperText; final bool isDestructive; final bool obscureText; final TextEditingController? controller; final Widget? prefixIcon; final Widget? suffixIcon; final TextInputType? keyboardType; final ValueChanged<String>? onChanged; final bool isPhoneField; final CountryCodePicker? countryCodePicker; const AppInputField({ super.key, required this.label, required this.placeholder, this.helperText = '', this.keyboardType, this.isDestructive = false, this.obscureText = false, this.controller, this.prefixIcon, this.suffixIcon, this.onChanged, this.isPhoneField = false, this.countryCodePicker, }); @override Widget build(BuildContext context) { final appTheme = SBTAppThemeProvider.watch(context); return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( label, style: AppTextStylesParagraph.paragraphSmallMedium.copyWith( color: appTheme.colorsScheme.secondary.appBarSecondaryText, ), ), const SizedBox(height: 4), Row( children: [ if (isPhoneField && countryCodePicker != null) countryCodePicker!, if (isPhoneField && countryCodePicker != null) const SizedBox(width: 8), Expanded( child: Container( decoration: BoxDecoration( color: appTheme.colorsScheme.primary.inputBg, borderRadius: BorderRadius.circular(16), border: Border.all( color: appTheme.colorsScheme.primary.inputBorderColor, width: 1, ), ), padding: const EdgeInsets.symmetric(horizontal: 12), child: Row( children: [ if (prefixIcon != null) prefixIcon!, Expanded( child: TextField( controller: controller, obscureText: obscureText, keyboardType: keyboardType, style: AppTextStylesParagraph.paragraphMediumMedium .copyWith( color: appTheme.colorsScheme.primary.textColor, ), onChanged: onChanged, decoration: InputDecoration( border: InputBorder.none, hintText: placeholder, hintStyle: AppTextStylesParagraph .paragraphMediumMedium .copyWith( color: appTheme.colorsScheme.primary.hintText, ), ), ), ), if (suffixIcon != null) suffixIcon!, ], ), ), ), ], ), if (helperText.isNotEmpty) Padding( padding: const EdgeInsets.only(top: 4.0), child: Text( helperText, key: ValueKey(helperText), style: TextStyle( color: isDestructive ? Colors.red : appTheme.colorsScheme.primary.hintText, fontSize: 12, ), ), ), ], ); } }
问题分析
- 组件重建导致状态丢失:
AppInputField是无状态组件,当外部触发setState(比如CountryCodePicker的回调)时,整个组件会重建,TextField的键盘类型可能在重建间隙出现短暂未赋值的情况。 - 系统默认关闭逻辑:点击键盘完成按钮时,Flutter的默认处理可能会先重置键盘类型,再执行关闭操作,导致视觉上的闪烁。
- 无稳定的组件标识:TextField没有设置唯一key,重建时无法准确复用原有状态,可能导致键盘配置被重置。
解决方案
方案1:优化TextField的焦点关闭逻辑
手动处理onSubmitted事件,直接关闭焦点,避免系统默认的键盘重置行为:
在AppInputField的TextField中添加以下配置:
TextField( // ... 原有配置 textInputAction: TextInputAction.done, onSubmitted: (_) { FocusScope.of(context).unfocus(); }, // ... )
方案2:给TextField添加稳定Key
确保TextField在重建时能保留状态,避免键盘类型重置:
TextField( key: ValueKey('${keyboardType}_${controller?.hashCode}'), // ... 原有配置 )
方案3:将AppInputField改为StatefulWidget
将无状态组件改为有状态,减少不必要的重建,同时结合前两种方案优化:
class AppInputField extends StatefulWidget { final String label; final String placeholder; final String helperText; final bool isDestructive; final bool obscureText; final TextEditingController? controller; final Widget? prefixIcon; final Widget? suffixIcon; final TextInputType? keyboardType; final ValueChanged<String>? onChanged; final bool isPhoneField; final CountryCodePicker? countryCodePicker; const AppInputField({ super.key, required this.label, required this.placeholder, this.helperText = '', this.keyboardType, this.isDestructive = false, this.obscureText = false, this.controller, this.prefixIcon, this.suffixIcon, this.onChanged, this.isPhoneField = false, this.countryCodePicker, }); @override State<AppInputField> createState() => _AppInputFieldState(); } class _AppInputFieldState extends State<AppInputField> { @override Widget build(BuildContext context) { final appTheme = SBTAppThemeProvider.watch(context); return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( widget.label, style: AppTextStylesParagraph.paragraphSmallMedium.copyWith( color: appTheme.colorsScheme.secondary.appBarSecondaryText, ), ), const SizedBox(height: 4), Row( children: [ if (widget.isPhoneField && widget.countryCodePicker != null) widget.countryCodePicker!, if (widget.isPhoneField && widget.countryCodePicker != null) const SizedBox(width: 8), Expanded( child: Container( decoration: BoxDecoration( color: appTheme.colorsScheme.primary.inputBg, borderRadius: BorderRadius.circular(16), border: Border.all( color: appTheme.colorsScheme.primary.inputBorderColor, width: 1, ), ), padding: const EdgeInsets.symmetric(horizontal: 12), child: Row( children: [ if (widget.prefixIcon != null) widget.prefixIcon!, Expanded( child: TextField( key: ValueKey('${widget.keyboardType}_${widget.controller?.hashCode}'), controller: widget.controller, obscureText: widget.obscureText, keyboardType: widget.keyboardType, textInputAction: TextInputAction.done, onSubmitted: (_) { FocusScope.of(context).unfocus(); }, style: AppTextStylesParagraph.paragraphMediumMedium .copyWith( color: appTheme.colorsScheme.primary.textColor, ), onChanged: widget.onChanged, decoration: InputDecoration( border: InputBorder.none, hintText: widget.placeholder, hintStyle: AppTextStylesParagraph .paragraphMediumMedium .copyWith( color: appTheme.colorsScheme.primary.hintText, ), ), ), ), if (widget.suffixIcon != null) widget.suffixIcon!, ], ), ), ), ], ), if (widget.helperText.isNotEmpty) Padding( padding: const EdgeInsets.only(top: 4.0), child: Text( widget.helperText, key: ValueKey(widget.helperText), style: TextStyle( color: widget.isDestructive ? Colors.red : appTheme.colorsScheme.primary.hintText, fontSize: 12, ), ), ), ], ); } }
方案4:缩小setState的作用范围
修改CountryCodePicker的回调,避免触发整个页面的setState,比如使用局部状态管理或状态管理框架(如Provider/BLoC)来管理国家代码,减少不必要的组件重建。
内容的提问来源于stack exchange,提问作者Aldiyarskiy
相关产品推荐
相关产品推荐

