2、TextFormField详解:属性、验证、样式与输入格式化
好,咱们今天来聊聊 TextFormField。这个组件在 Flutter 表单里,可以说是出镜率最高的一个了。我刚开始用 Flutter 写表单的时候,觉得它不就是个输入框嘛,有啥好讲的?结果后来踩了不少坑,才发现这里面的门道还真不少。
说白了,TextFormField 是 TextField 的「表单增强版」。它直接继承自 FormField,所以天然就支持表单验证、状态管理这些能力。你想想看,如果只是用 TextField,你得自己手动维护验证逻辑、错误状态,那多麻烦。
2.1 核心属性一览
咱们先过一遍最常用的属性。我习惯把它们分成三类:控制类、展示类、行为类。
| 分类 | 属性名 | 作用 |
|---|---|---|
| 控制类 | controller | 绑定 TextEditingController,控制输入内容 |
| 控制类 | focusNode | 管理焦点状态 |
| 展示类 | decoration | 输入框的样式、标签、提示文字等 |
| 展示类 | style | 输入文字的样式 |
| 行为类 | keyboardType | 键盘类型(数字、邮箱、电话等) |
| 行为类 | textInputAction | 键盘右下角按钮行为(下一步、搜索等) |
| 验证类 | validator | 验证函数,返回错误字符串 |
| 验证类 | autovalidateMode | 自动验证的触发时机 |
嗯,这里我要特别提一下 autovalidateMode。我在项目中遇到过好几次,用户刚进入页面还没输入呢,错误提示就冒出来了,体验很差。后来我统一设成了 AutovalidateMode.onUserInteraction,只有用户开始交互后才触发验证,这才舒服了。
2.2 验证机制详解
验证是 TextFormField 最核心的能力。它的工作方式其实很简单:你提供一个 validator 函数,返回 null 表示通过,返回字符串表示错误信息。
TextFormField(
validator: (value) {
if (value == null || value.isEmpty) {
return '请输入内容';
}
if (value.length < 6) {
return '至少输入6个字符';
}
return null;
},
)
为什么会这样设计?因为 Flutter 把验证逻辑完全交给了开发者,灵活性非常高。你可以在这里做任何检查:正则匹配、远程校验、甚至调用 API 验证。
我曾经做过一个注册表单,需要验证用户名是否已被注册。我就在 validator 里调了个异步方法,配合 asyncValidator 属性来处理。不过要注意,异步验证会带来 UI 延迟,最好配合加载状态一起使用。
2.3 样式定制
样式这块,主要靠 decoration 属性。它是 InputDecoration 类型,能控制的东西非常多。
TextFormField(
decoration: InputDecoration(
labelText: '用户名',
hintText: '请输入您的用户名',
prefixIcon: Icon(Icons.person),
suffixIcon: Icon(Icons.clear),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
),
focusedBorder: OutlineInputBorder(
borderSide: BorderSide(color: Colors.blue, width: 2),
),
errorBorder: OutlineInputBorder(
borderSide: BorderSide(color: Colors.red),
),
),
)
我个人习惯把 focusedBorder 和 errorBorder 都显式定义出来。为什么?因为默认的样式在不同平台上表现不一致。Android 和 iOS 的 Material 风格就有差异,你想想看,如果用户从 iOS 换到 Android,输入框突然变了个样,那多奇怪。
还有一点,labelText 和 hintText 的区别要搞清楚。labelText 是浮动标签,输入后会上移;hintText 是占位提示,输入后就消失。我见过不少新手把两者混用,结果 UI 表现很奇怪。
2.4 输入格式化
输入格式化,说白了就是限制用户输入的内容。比如手机号只能输数字、金额只能输两位小数、身份证号要自动加空格等等。
Flutter 提供了 inputFormatters 属性,接受一个 TextInputFormatter 列表。系统内置了几个常用的:
FilteringTextInputFormatter.digitsOnly:只允许数字FilteringTextInputFormatter.allow(RegExp):只允许匹配正则的字符FilteringTextInputFormatter.deny(RegExp):禁止匹配正则的字符LengthLimitingTextInputFormatter:限制最大长度
TextFormField(
keyboardType: TextInputType.number,
inputFormatters: [
FilteringTextInputFormatter.digitsOnly,
LengthLimitingTextInputFormatter(11),
],
decoration: InputDecoration(
labelText: '手机号',
),
)
但有时候内置的满足不了需求。比如格式化银行卡号,每4位加一个空格。这时候就需要自定义 Formatter 了。
class CardNumberFormatter extends TextInputFormatter {
@override
TextEditingValue formatEditUpdate(
TextEditingValue oldValue,
TextEditingValue newValue,
) {
// 只保留数字
String digits = newValue.text.replaceAll(RegExp(r'\D'), '');
// 每4位加空格
StringBuffer buffer = StringBuffer();
for (int i = 0; i < digits.length; i++) {
if (i > 0 && i % 4 == 0) {
buffer.write(' ');
}
buffer.write(digits[i]);
}
return TextEditingValue(
text: buffer.toString(),
selection: TextSelection.collapsed(offset: buffer.length),
);
}
}
嗯,这里要注意光标位置的处理。我刚开始写的时候忘了更新 selection,结果用户输入时光标总是跳到最前面,体验非常糟糕。后来我养成了习惯,每次格式化后都重新计算光标位置。
2.5 实战中的避坑指南
最后分享几个我踩过的坑,希望能帮你少走弯路。
- controller 的生命周期: 如果你在 StatefulWidget 里创建了 TextEditingController,记得在 dispose 里释放它。否则会有内存泄漏的风险。
- onChanged 和 validator 的执行顺序: onChanged 先执行,validator 后执行。所以不要在 onChanged 里做验证逻辑,那是 validator 的活。
- 键盘类型和格式化冲突: 比如你设了 keyboardType 为 TextInputType.number,又用 FilteringTextInputFormatter.allow 只允许字母,那键盘和格式化就会打架。建议保持一致。
我曾经在一个金融项目里,需要输入金额。我设了数字键盘,又用 Formatter 限制了两位小数。结果用户输入小数点时,键盘上的小数点按钮和 Formatter 的正则冲突了,导致小数点死活输不进去。排查了半天才发现是正则写得太死了。
所以我的建议是:能用 keyboardType 解决的,就别用 Formatter;用 Formatter 的,就别太依赖 keyboardType。两者配合使用时,一定要测试边界情况。
好了,TextFormField 的核心内容就这些。下一节咱们聊聊 Form 的整体管理和多字段联动,那才是真正体现水平的地方。