中文输入必踩的 Flutter 坑合集:iOS 拼音打不出来,其实是你 Formatter 写错了
如果你在 Flutter 里做过「只允许中文 / 中英文校验」,并且只在 iOS 上翻过车,那这篇文章大概率能帮你节省半天 Debug 时间。
这不是 iOS 的锅,也不是 Flutter 的 Bug,而是 TextInputFormatter 和中文输入法(IME)之间的理解偏差。
一、血iOS 上拼音怎么都打不出来
常见反馈包括:
- iOS 中文拼音键盘
- 输入
bei jing - 键盘有拼音显示
- 输入框内容完全不变
- 无法选词、无法上屏
👉 Android 正常
👉 模拟器正常
👉 真机 iOS 不行
很多人第一反应是:
"Flutter 对中文支持不好?"
结论先行:不是。
二、罪魁祸首:TextInputFormatter 的「中文校验」
下面这种 Formatter,你一定写过或见过:
scala
class NameInputFormatter extends TextInputFormatter {
@override
TextEditingValue formatEditUpdate(
TextEditingValue oldValue,
TextEditingValue newValue,
) {
final chineseOnly = RegExp(r'^[\u4E00-\u9FFF]+$');
if (newValue.text.isEmpty) return newValue;
if (!chineseOnly.hasMatch(newValue.text)) {
return oldValue; //
}
return TextEditingValue(
text: newValue.text,
selection: TextSelection.collapsed(
offset: newValue.text.length,
),
);
}
}
逻辑看起来非常合理:
- 只允许中文
- 非法字符直接回退
但在 iOS 上,这段代码等于封死了中文输入法的入口。
三、核心原理:iOS 中文输入法有「组字阶段」
1️ composing 是什么?
iOS 拼音输入法的输入过程分为两步:
-
组字(composing)
- 输入:
bei - 输入框里是拼音(未确认)
- 输入:
-
提交
- 选择「北」
- 中文字符真正上屏
在组字阶段:
ini
newValue.text == "bei"
newValue.composing.isCollapsed == false
而 "bei" 必然无法通过「只允许中文」的正则校验。
2️ Formatter 提前"否决"了输入
当 Formatter 在 composing 阶段做了以下任意一件事:
return oldValue- 修改
text - 强制重置
selection
iOS 输入法就会认为:
「当前输入不合法,终止组字」
于是出现经典现象:
拼音能打,但永远无法选字
四、隐藏更深的坑:selection 会杀死输入法
很多 Formatter 里都有这行:
vbnet
selection: TextSelection.collapsed(offset: text.length),
在普通输入下没问题,但在中文输入中:
- selection 是 IME 状态的一部分
- 每次重置 selection = 重启组字流程
哪怕你放行了拼音,也可能出现:
- 候选词异常
- 游标跳动
- 输入体验极差
五、那为什么 Android 没这个问题?
这是一个非常关键、也最容易误判的点。
Android 的行为差异
- Android 输入法对 composing 的暴露不一致
- 很多键盘在 字符提交后才触发 Formatter
- 即使 composing 存在,也更"宽容"
结果就是:
错误的 Formatter 在 Android 上"看起来能用"
但这并不代表代码是对的,只是 Android 没那么严格。
真相
Android 是侥幸没炸,iOS 是严格把问题暴露出来。
六、正确原则
1. composing 阶段必须放行
kotlin
if (!newValue.composing.isCollapsed) {
return newValue;
}
2. 校验只在 composing 结束后做
3. 不要无脑重置 selection
4. Formatter ≠ 表单最终校验
七、正确示例
下面是一个安全、可扩展、iOS / Android 双端稳定的 Formatter 示例:
python
class UniversityNameInputFormatter extends TextInputFormatter {
UniversityNameInputFormatter({this.maxLength = 40});
final int maxLength;
static final RegExp _disallowed =
RegExp(r'[^a-zA-Z0-9\u4E00-\u9FFF-\s]');
static final RegExp _multiHyphen = RegExp(r'-{2,}');
static final RegExp _leadingHyphen = RegExp(r'^-+');
static final RegExp _trailingHyphen = RegExp(r'-+$');
@override
TextEditingValue formatEditUpdate(
TextEditingValue oldValue,
TextEditingValue newValue,
) {
// iOS 中文拼音组字阶段
if (!newValue.composing.isCollapsed) {
return newValue;
}
var text = newValue.text;
if (text.isEmpty) return newValue;
text = text.replaceAll(_disallowed, '');
text = text.replaceAll(_multiHyphen, '-');
text = text.replaceAll(_leadingHyphen, '');
text = text.replaceAll(_trailingHyphen, '');
if (text.length > maxLength) {
text = text.substring(0, maxLength);
}
if (text == newValue.text) return newValue;
int clamp(int o) => o.clamp(0, text.length);
return TextEditingValue(
text: text,
selection: TextSelection(
baseOffset: clamp(newValue.selection.baseOffset),
extentOffset: clamp(newValue.selection.extentOffset),
),
composing: TextRange.empty,
);
}
}
八、中文输入必踩的 Flutter 坑合集(Checklist)
❌ 坑 1:Formatter 里直接做中文正则校验
后果:iOS 拼音无法输入
❌ 坑 2:忽略 newValue.composing
后果:IME 组字被打断
❌ 坑 3:每次都把 selection 移到末尾
后果:候选词异常、游标乱跳
❌ 坑 4:以为 Android 正常 = 代码正确
后果:iOS 真机翻车
九、一句话总结
TextInputFormatter 是 IME 输入流程的一部分,不是简单的字符串过滤器。