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

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,
              ),
            ),
          ),
      ],
    );
  }
}

问题分析

  1. 组件重建导致状态丢失:AppInputField是无状态组件,当外部触发setState(比如CountryCodePicker的回调)时,整个组件会重建,TextField的键盘类型可能在重建间隙出现短暂未赋值的情况。
  2. 系统默认关闭逻辑:点击键盘完成按钮时,Flutter的默认处理可能会先重置键盘类型,再执行关闭操作,导致视觉上的闪烁。
  3. 无稳定的组件标识: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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.16 05:04:54