ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Flutter 鸿蒙跨端实践:TextFormField 输入格式化全解析

Flutter 鸿蒙跨端实践:TextFormField 输入格式化全解析 这两年做跨端应用我几乎每次都要回答同一个问题Flutter 在鸿蒙上到底能不能跑表单项多不多体验稳不稳。我的答案是能跑而且表单密集型场景反而更容易做稳——前提是你得把TextFormField的输入格式化吃透。这篇文章不聊空泛的“鸿蒙生态”只讲我在鸿蒙设备和安卓设备上都验证过的TextFormField格式化方案包括手机号、金额千分位、银行卡分段这类高频需求以及输入法组合区、光标跳动、键盘顶起这些绕不开的坑。适合正在做 Flutter 鸿蒙跨端改造、或者想系统梳理输入格式化原理的开发者。1. 为什么 Flutter 和鸿蒙的组合值得认真对待1.1 鸿蒙跨端选型时 Flutter 的优势鸿蒙生态这两年的变化做客户端的都有体感一方面系统装机量上来了另一方面很多存量 App 不可能为它单独维护一套原生代码。跨端框架就成了性价比选项而 Flutter 在其中算是“底子”最接近原生体验的那一个。它在鸿蒙上不是简单套个 WebView而是有官方维护的鸿蒙分支Dart 代码可以直接编译进 HAP 包UI 渲染走自绘引擎不依赖系统控件。这就意味着你在安卓上写的界面和交互逻辑拿到鸿蒙上基本不用动。但这不代表没门槛。跨端框架在不同系统上最容易出问题的反而就是细节输入法怎么配合、键盘弹起怎么处理、安全区怎么避让。这些细节在表单场景里会被无限放大。你想想一个页面三五个输入框手机号要分段展示、金额要千分位、银行卡号要 4 位一组如果输入框和系统输入法配合不好用户输到一半光标乱跳这个 App 基本就废了。TextFormField是 Flutter 表单体系里的核心组件它在TextField基础之上多了校验、表单联动这些能力。我做鸿蒙适配时最大的体会是与其在业务代码里到处写onChanged做字符串处理不如把格式化的逻辑统一收敛到inputFormatters这一层。原因后面展开简单说就是——它能做到“边输入边格式化”而且不会因为 setState 导致输入框重建、光标丢失。这套机制理解透了你在鸿蒙、安卓、iOS 上写的表单逻辑可以完全复用这才是跨端的价值。1.2 表单场景里 TextFormField 的核心价值TextFormField比TextField多出来的核心能力是它绑定了FormFieldState。具体来说它可以配合Form做整页校验validator可以在输入过程中按需触发autovalidateMode可以控制校验时机。这些能力在跨端工程里特别重要因为原生端通常要自己维护一堆校验状态而 Flutter 帮你收敛了。不过我看过不少项目把validator和inputFormatters混为一谈——实际上它们是两件事。validator只负责“这个值合不合法”不负责“这个值长什么样”inputFormatters负责“用户在输入时文本内容如何被修正和限制”。比如手机号输入框inputFormatters负责让用户只能输入数字、自动插入空格、限制 11 位而validator负责最后检查这 11 位是不是一个合法的手机号段。两者顺序执行、互不替代。理解了这层关系你就知道为什么我推荐把格式化逻辑放进inputFormatters而不是onChanged。onChanged触发时文本已经落到 controller 里了你想改它就必须二次赋值而赋值动作又会触发onChanged处理不好就是死循环就算你用标志位挡掉光标位置也非常难维护。inputFormatters则是在文本进入编辑器之前拦截改完再把处理后的TextEditingValue交给输入框整个过程不会造成控制器回写也不需要 setState。2. TextFormField 输入格式化先搞懂它在哪里生效2.1 inputFormatters 的执行链路TextFormField的inputFormatters参数接收一个ListTextInputFormatter。每一次用户键入、删除、粘贴、选择替换Flutter 引擎都会把当前的编辑状态打包成TextEditingValue包含文本内容、光标选区、输入法组合区然后按顺序传给这个列表里的每一个 formatter。这里有个重点formatter 接收到的不是“旧值”和“新字符”而是“旧值”和“新值”。TextEditingValue oldValue是这次编辑发生前的完整状态TextEditingValue newValue是编辑发生后、还没经过格式化的状态。你做的事情就是基于这两个值计算出“最终应该呈现的”TextEditingValue并返回。返回值会交给下一个 formatter最后一个 formatter 的返回值才会真正显示在输入框里。这个链路的含义是链表里的 formatter 是有顺序的前一个的输出是后一个的输入。我见过不少翻车案例都是把FilteringTextInputFormatter和自定义格式化函数顺序放反结果过滤完的文本又被长度限制截断导致手机号格式化算出来的位置对不上。后面我会给一个推荐的配置顺序。2.2 三个内置格式化工具的正确用法Flutter 内置了三个高频使用的 formatter先弄清楚它们的定位能少写很多代码。第一个是FilteringTextInputFormatter。它有两个命名构造函数FilteringTextInputFormatter.allow(RegExp)表示只允许匹配正则的字符FilteringTextInputFormatter.deny(RegExp)表示禁止匹配正则的字符。注意它是逐字符过滤的也就是说粘贴一串字符进来它会先把不合法字符删掉再放行。比如限制只能输入数字就用FilteringTextInputFormatter.allow(RegExp(r[0-9]))。第二个是LengthLimitingTextInputFormatter。它用来限制输入长度按字符数计数。它和 TextFormField 自带的maxLength参数功能相近但两者同时设置时会重复叠加一个限制器而且maxLength会在输入框右下角显示计数部分场景有点丑。我建议二选一业务里用maxLength更省事需要精细控制时用 formatter。第三个是TextInputFormatter.withFunction。它是一个便捷封装可以直接传一个函数来定义格式化规则。适合简单场景比如把输入的小写字母自动转大写TextInputFormatter.withFunction((oldValue, newValue) newValue.copyWith(text: newValue.text.toUpperCase()))。注意内置 formatter 处理不了“插入分隔符”这类需求。手机号加空格、金额加千分位都不能靠单个正则完成因为每输入一位数字分隔符的位置就要整体重排这个时候必须写自定义 formatter。2.3 自定义 formatter 的完整接口与代码骨架自定义 formatter 的标准做法是实现TextInputFormatter接口重写formatEditUpdate方法。我建议所有格式化逻辑都走这个接口而不是用withFunction因为复杂场景下你还需要处理光标位置映射、输入法组合区、删除边界这些逻辑写在一个类里更清爽。先看清楚返回值的核心结构。TextEditingValue有三个关键字段text输入框当前展示的完整文本selection光标位置通常用TextSelection.collapsed(offset: index)表示composing输入法组合区用于标记拼音、候选词等正在组装的文本区间自定义 formatter 最容易犯的错误是只改text忘了改selection。你想想用户正输到第 5 个字符你突然在中间插了一个空格文本长度变了光标位置却没变那光标就会跳到空格前或者跑到文本外面用户下一位数字就打在了奇怪的位置。所以凡是会改变字符串长度的 formatter都必须重新计算光标位置。另一个高频错误是返回的composing处理不当。中文输入法在拼音组词阶段会通过composing标记一段“还没上屏”的文本如果 formatter 无脑把composing清空鸿蒙和安卓上的中文输入法很容易崩溃或者候选词直接消失。正确做法是检测到oldValue.composing.isValid newValue.composing.isValid时直接返回newValue不做任何格式化。我总结的代码骨架长这样import package:flutter/services.dart; class BaseInputFormatter extends TextInputFormatter { override TextEditingValue formatEditUpdate( TextEditingValue oldValue, TextEditingValue newValue, ) { // 1. 输入法组合过程中不干预 if (oldValue.composing.isValid || newValue.composing.isValid) { return newValue; } // 2. 文本没变化比如只是光标移动直接返回 if (newValue.text oldValue.text) { return newValue; } // 3. 对 newValue.text 做格式化逻辑得到新的文本 // 4. 如果格式化后文本没有变化直接返回 newValue // 注意这里要保留 newValue 的光标位置 // 5. 如果文本变化了重新计算光标返回新的 TextEditingValue } }这套骨架我在鸿蒙分支上用了很久稳定性是有保障的。下面几个实战场景都基于这个骨架展开。3. 高频格式化场景完整实现3.1 手机号 3-4-4 格式与光标保持手机号格式化是表单里最典型的场景用户在绝大多数 App 里都习惯了138 1234 5678这种展示方式。实现逻辑不复杂去掉所有非数字字符截断到 11 位再按 3-4-4 插入空格。难点全在光标位置的换算上。我当时踩过一个坑用户输入到第 12 位时格式化逻辑把超出的那一位截掉了但我没有把光标挪到第 11 位末尾结果用户每多按一下光标就往后跑一位屏幕上是1381234实际值已经乱了。后来我总结出一个稳妥的光标映射思路取原始文本中光标前的一截去掉其中的分隔符数出有效字符个数再到格式化后的文本里从头数出同样多有效字符的位置作为新光标位置。class PhoneInputFormatter extends TextInputFormatter { override TextEditingValue formatEditUpdate( TextEditingValue oldValue, TextEditingValue newValue, ) { // 输入法组合中不干预避免中文输入法候选词被破坏 if (oldValue.composing.isValid || newValue.composing.isValid) { return newValue; } // 仅光标移动文本没变化直接返回 if (newValue.text oldValue.text) { return newValue; } // 1. 去掉所有非数字字符 final digits newValue.text.replaceAll(RegExp(r[^0-9]), ); // 2. 限制最多 11 位 final limited digits.length 11 ? digits.substring(0, 11) : digits; // 3. 按 3-4-4 插入空格 final buffer StringBuffer(); for (int i 0; i limited.length; i) { if (i 3 || i 7) { buffer.write( ); } buffer.write(limited[i]); } final formatted buffer.toString(); if (formatted newValue.text) { return newValue; } // 4. 重新计算光标位置 final newOffset _mapOffset( original: newValue.text, formatted: formatted, originalOffset: newValue.selection.baseOffset, ); return TextEditingValue( text: formatted, selection: TextSelection.collapsed(offset: newOffset), composing: TextRange.empty, ); } int _mapOffset({ required String original, required String formatted, required int originalOffset, }) { if (originalOffset 0) return formatted.length; // 原始光标前的所有字符去掉非数字后得到有效字符个数 final prefix original.substring(0, originalOffset); final meaningfulCount prefix.replaceAll(RegExp(r[^0-9]), ).length; // 在格式化文本中从头数 meaningfulCount 个有效字符 int count 0; int pos 0; while (pos formatted.length count meaningfulCount) { if (RegExp(r[0-9]).hasMatch(formatted[pos])) { count; } pos; } return pos; } }这段代码在键盘类型设置为TextInputType.number时中文输入法基本不会介入但为了保险composing判断还是留着了。鸿蒙自带输入法在某些机型上对数字键盘的细节封装不一样留着这层保护不会出错。使用时候的配置TextFormField( keyboardType: TextInputType.number, inputFormatters: [ PhoneInputFormatter(), ], decoration: InputDecoration( hintText: 请输入手机号, prefixIcon: Icon(Icons.phone), ), )这里有个细节keyboardType要和 formatter 的逻辑一致。如果手机号字段同时支持国外号码你可能不希望只允许数字那可以把键盘类型改成TextInputType.phoneformatter 里保留和-的处理逻辑。我见过海外 App 需求手机号允许86 138-1234-5678这种形式那你的分隔规则就要从“固定 3-4-4”改成“按实际长度自适应”核心的光标映射逻辑不用变。3.2 金额千分位格式化实时加小数点金额输入框是另一个高频难点。用户要看到12,345.67这种千分位效果而不是输完再统一格式化。这个场景比手机号复杂因为牵扯到小数点而且小数部分是保留 2 位还是允许自由输入不同业务不一样。我建议的做法是先把文本里的逗号全部去掉得到原始数字串校验它是不是合法的金额输入数字和至多一个小数点然后整数部分从右往左每 3 位加一个逗号小数部分保持不动。光标位置的计算和手机号类似但有效字符要包含小数点。class AmountInputFormatter extends TextInputFormatter { final int maxIntegerDigits; AmountInputFormatter({this.maxIntegerDigits 9}); static final RegExp _allowedRaw RegExp(r^\d*\.?\d*$); override TextEditingValue formatEditUpdate( TextEditingValue oldValue, TextEditingValue newValue, ) { if (oldValue.composing.isValid || newValue.composing.isValid) { return newValue; } // 1. 去掉千分位逗号得到原始输入串 final raw newValue.text.replaceAll(,, ); // 2. 非法字符直接拒绝本次输入 if (!_allowedRaw.hasMatch(raw)) { return oldValue; } // 3. 限制整数部分长度 final dotIndex raw.indexOf(.); final integerPart dotIndex -1 ? raw : raw.substring(0, dotIndex); if (integerPart.length maxIntegerDigits) { return oldValue; } // 4. 去逗号后内容和上次一样说明只是光标移动保持原样 if (raw oldValue.text.replaceAll(,, )) { return newValue; } // 5. 整数部分加千分位 final buffer StringBuffer(); for (int i 0; i integerPart.length; i) { if (i 0 (integerPart.length - i) % 3 0) { buffer.write(,); } buffer.write(integerPart[i]); } var formatted buffer.toString(); if (dotIndex ! -1) { formatted .; formatted raw.substring(dotIndex 1); } if (formatted newValue.text) { return newValue; } // 6. 换算光标位置 final newOffset _mapOffset( original: newValue.text, formatted: formatted, originalOffset: newValue.selection.baseOffset, ); return TextEditingValue( text: formatted, selection: TextSelection.collapsed(offset: newOffset), composing: TextRange.empty, ); } int _mapOffset({ required String original, required String formatted, required int originalOffset, }) { if (originalOffset 0) return formatted.length; final prefix original.substring(0, originalOffset); final meaningfulCount prefix.replaceAll(RegExp(r[^0-9.]), ).length; int count 0; int pos 0; while (pos formatted.length count meaningfulCount) { if (RegExp(r[0-9.]).hasMatch(formatted[pos])) { count; } pos; } return pos; } }使用的时候键盘类型要设置成带小数点的数字键盘TextFormField( keyboardType: const TextInputType.numberWithOptions(decimal: true), inputFormatters: [AmountInputFormatter(maxIntegerDigits: 7)], textAlign: TextAlign.right, )金额输入框还有一个容易被忽略的点你需要在获取最终值时把展示用的逗号去掉再转成业务侧需要的数值类型。最稳妥的方式是不要直接double.parse(text)而是先text.replaceAll(,, )再解析否则逗号会导致解析异常。3.3 银行卡号与身份证的分段录入银行卡号格式化本质上和手机号一样只不过分隔规则变成了 4 位一组长度通常在 16 到 19 位之间。可以直接复用上一节的光标映射思路只改空格插入位置。代码里我会写一个更通用的分段 formatter最多支持 19 位class GroupFormatter extends TextInputFormatter { final int groupSize; final int maxLength; GroupFormatter({this.groupSize 4, this.maxLength 19}); override TextEditingValue formatEditUpdate( TextEditingValue oldValue, TextEditingValue newValue, ) { if (oldValue.composing.isValid || newValue.composing.isValid) { return newValue; } if (newValue.text oldValue.text) { return newValue; } final digits newValue.text.replaceAll(RegExp(r[^0-9]), ); final limited digits.length maxLength ? digits.substring(0, maxLength) : digits; final buffer StringBuffer(); for (int i 0; i limited.length; i) { if (i 0 i % groupSize 0) { buffer.write( ); } buffer.write(limited[i]); } final formatted buffer.toString(); if (formatted newValue.text) { return newValue; } int meaningfulCount newValue.text .substring(0, newValue.selection.baseOffset) .replaceAll(RegExp(r[^0-9]), ) .length; int count 0; int pos 0; while (pos formatted.length count meaningfulCount) { if (RegExp(r[0-9]).hasMatch(formatted[pos])) { count; } pos; } return TextEditingValue( text: formatted, selection: TextSelection.collapsed(offset: pos), composing: TextRange.empty, ); } }身份证输入有个不一样的点末位允许是X而且用户常会用小写x。我的方案是允许输入数字和Xx然后在自定义 formatter 里把小写统一转大写同时限制 18 位。这样用户输小写也能通过展示时是标准的大写。身份证如果需要更友好的展示可以尝试110101 19900101 1234这种分段前 6 位地区码、第 7 到 14 位生日、最后 4 位顺序码加校验位。不过说句实在话身份证输入框大部分业务不需要强制分段展示因为用户对身份证号的记忆分组不明显分段反而可能影响核对。我做项目时一般只在末尾把x转大写不做空格分隔除非产品明确要求。3.4 formatter 与 validator 的配合坑很多新手容易混淆 formatter 和 validator 的职责导致业务数据出问题。记住一点formatter 只解决“输入什么样”validator 只解决“输入值是否合法”两者的执行时机完全不同。inputFormatters在每次编辑时都会执行validator只在提交校验或者autovalidateMode触发时执行。我在鸿蒙项目里踩过一个具体坑手机号输入框用了格式化保存时直接formState.validate()validator 里写的正则却是^1[3-9]\d{9}$手机号138 1234 5678带空格永远校验不过。后来改成先清理空格再校验validator: (value) { final clean (value ?? ).replaceAll(RegExp(r\s), ); if (clean.isEmpty) return 手机号不能为空; if (!RegExp(r^1[3-9]\d{9}$).hasMatch(clean)) { return 手机号格式不正确; } return null; },这个坑在金额输入框上更隐蔽。用户看到的是12,345.67validator 拿到的是12,345.67如果直接double.tryParse解析是会失败的因为double.tryParse不认识千分位逗号。必须先replaceAll(,, )。另外一个建议autovalidateMode不要用AutovalidateMode.always一边输入一边校验收效体验太差用AutovalidateMode.onUserInteraction更合适用户停手时才触发输入过程不打扰。4. 鸿蒙端适配的细节与踩坑4.1 从 flutter create 到鸿蒙 HAP 跑通在鸿蒙上跑 Flutter 工程第一步是确认你用的 Flutter SDK 是什么分支。华为和 OpenHarmony 社区目前提供的鸿蒙适配分支用法和官方 Flutter SDK 基本一致只是支持了ohos这个平台。搭建环境时你需要装好 DevEco Studio 和 HarmonyOS SDK然后把 Flutter 鸿蒙分支解压到本地配置好环境变量。创建项目时如果环境已经指向鸿蒙分支可以这样初始化flutter create --platforms ohos my_app cd my_app如果项目已经存在想补上 ohos 平台目录在项目根目录执行flutter create --platforms ohos .这一步会生成ohos目录里面是鸿蒙原生工程结构。之后用 DevEco Studio 打开ohos目录配置好签名就能跑真机。签名这块调试阶段用自动签名就行发布才需要申请正式证书。我在实际跑项目时遇到最多的不是编译失败而是“flutter run 找不到设备”。鸿蒙真机需要开启开发者模式并连接 ADB而 DevEco Studio 用的是 hdc 工具两边如果没配合好Flutter 工具链会识别不到设备。解决办法是用 DevEco Studio 里的 hdc 手动连接设备确认hdc list targets能看到设备再回到命令行跑flutter run。4.2 键盘弹起与安全区的处理跨端开发里输入框最影响体验的问题就是键盘弹起时输入框被遮住。Flutter 的Scaffold默认resizeToAvoidBottomInset: true理论上有滚动容器就能自动把内容顶上去。鸿蒙上和安卓表现接近但我发现有些页面如果你用了全屏自定义布局或者Scaffold嵌套层级太深键盘顶起会失效。排查这类问题第一步先看MediaQuery.of(context).viewInsets.bottom有没有随键盘变化。这个值在键盘弹出时会变成键盘高度如果有值但界面不弹说明容器没有跟着响应如果一直是 0说明键盘事件没有正确传导到 Flutter 侧需要检查鸿蒙原生工程的 window 设置。另外一个细节鸿蒙部分设备有键盘的“安全区避让”策略也就是键盘弹起时应用不会被整体压缩而是窗口调整到键盘上方。Flutter 侧拿到的是调整后的窗口尺寸所以viewInsets.bottom可能为 0但窗口本身已经变小了。这种情况你要用MediaQuery.of(context).size的变化来感知键盘状态我自己就遇到过在Scaffold正常使用时没影响但一旦自定义SafeArea就会出问题。提示做登录、注册这类多输入框页面建议外层套SingleChildScrollView并且把resizeToAvoidBottomInset保持默认。不要为了 UI 好看手动固定高度键盘一开就会露馅。4.3 鸿蒙输入法组合区composing的兼容这一节是整篇文章里我最想强调的。中文输入法在鸿蒙和安卓上的行为很特殊用户打拼音时拼音串和候选词会以“组合区”的形式存在Flutter 通过TextRange把它标记出来。如果你写的 formatter 每次输入都创建一个新的TextEditingValue并且把composing清空输入法会认为上屏流程被破坏轻则候选词消失重则整个输入框卡死。我在鸿蒙真机上复现过这个问题手机号输入框在拼音输入法下用户先按了一个字母候选词弹出来一瞬间输入框直接失去了响应必须重新聚焦才能继续输入。定位后发现是 formatter 里无条件把composing设成了TextRange.empty。修复方案就是前面代码里反复出现的那行判断if (oldValue.composing.isValid || newValue.composing.isValid) { return newValue; }原理是组合期间文本还在“中间态”格式化没意义而且很容易破坏输入法状态机。等用户选词上屏composing变成无效formatter 再正常干活。这个判断对纯数字键盘影响不大但对中文/英文键盘至关重要手机号、金额这些字段在鸿蒙端也建议保留。5. 常见异常与处理方案速查5.1 光标乱跳和输入法崩溃格式化导致光标乱跳绝大多数原因是返回的selection没有按新文本长度重新计算。我见过一个最典型的错误是formatter 里return newValue.copyWith(text: formatted)光标位置还是老的结果用户输入第 4 位数字时文本长度加了一个空格光标却被顶到空格后面用户再按删除键会把数字删掉而不是删除空格。正确的处理我在手机号 formatter 里已经给了核心就是用“有效字符数”来映射光标而不是用字符下标硬减。输入法崩溃的排查顺序我建议这样走第一步检查 formatter 是否在composing有效时动了text第二步检查返回的composing是否被错误清空第三步检查是否在 formatter 里抛了异常——formatter 里的异常不会被输入框架兜住经常是直接闪退。5.2 粘贴与性能问题用户从别处复制一段文本直接粘贴到输入框formatter 会一次性处理一长串字符。这个场景在手机号、银行卡上问题不大因为长度限制很快截断但金额输入框如果粘贴了123,456,789.00格式化循环要处理十几个字符性能影响不大。真正危险的是有人用嵌套量词的正则去校验整段文本。比如RegExp(r^((\d{1,3})(,\d{3})*)$)这种正则看起来能校验千分位格式但嵌套量词在长文本上可能产生灾难性回溯导致输入卡顿几秒钟。在 formatter 里做校验宁可多写几行普通代码也不要写带嵌套量词的复杂正则。我的金额校验只用^\d*\.?\d*$量词简单不会回溯。粘贴场景还有个细节LengthLimitingTextInputFormatter对粘贴文本也是一次性截断如果你的自定义 formatter 在它后面执行截断后的文本可能不是你期望的格式。比如限制 11 位手机号用户在输入框里粘贴138123456789999长度限制会先截成138123456789但你自定义分组逻辑想按 11 位截就会多出来一位。解决方案是自定义 formatter 放在长度限制前面自己处理长度截断或者干脆不使用内置长度限制在自定义格式逻辑里统一做。5.3 排查建议与现场经验做鸿蒙端 Flutter 输入框排查我建议先写一个最小复现页面只放一个TextFormField把所有业务装饰都去掉然后逐个测试英文输入、中文输入、数字键盘、删除到空、粘贴超长文本。不要直接在复杂页面里调因为页面布局、键盘策略、其他 formatter 都会干扰定位。实测下来我在鸿蒙端遇到了一个安卓上没有的小问题部分鸿蒙版本对TextInputType.numberWithOptions(decimal: true)的支持不完整弹出的数字键盘可能没有小数点按键导致金额输入框无法输入.。这种情况要么改用全键盘加FilteringTextInputFormatter.allow限位要么在 formatter 里兼容处理判断用户输入的是不是系统键盘映射的字符。不同设备行为有差异做兼容方案时最好覆盖这一层。汇总一下常见问题和对应方案问题原因解决方案输入中文时候选词消失/卡死formatter 改动 composing 区检测 composing.isValid 直接返回删除时数字莫名消失光标位置没有随文本长度更新用有效字符数映射光标校验永远不过validator 没处理格式化字符先清理空格/逗号再校验粘贴文本出现多余字符长度限制器和自定义 formatter 顺序不对自定义 formatter 优先内部处理截断金额键盘没有小数点鸿蒙端对 decimal 类型支持差异改用普通键盘过滤正则屏蔽非法字符键盘弹起页面不避让Scaffold 嵌套或自定义布局干扰查 viewInsets外层包滚动容器我个人的体会是格式化的代码本身不难难的是把“边界情况”想清楚尤其是删除操作、光标移动、输入法组合状态、粘贴超长文本这四件事。你把这四个边界都处理干净了TextFormField在鸿蒙和安卓上的行为就是一致的跨端的优势才真正发挥出来。最后再分享一个小技巧写 formatter 时每一步都先问自己一句“如果我是用户我现在的光标在哪儿我刚按的是哪个键”很多隐蔽 bug 都能在写代码阶段自己发现。
返回列表