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),
    ),
  ),
)

我个人习惯把 focusedBordererrorBorder 都显式定义出来。为什么?因为默认的样式在不同平台上表现不一致。Android 和 iOS 的 Material 风格就有差异,你想想看,如果用户从 iOS 换到 Android,输入框突然变了个样,那多奇怪。

还有一点,labelTexthintText 的区别要搞清楚。labelText 是浮动标签,输入后会上移;hintText 是占位提示,输入后就消失。我见过不少新手把两者混用,结果 UI 表现很奇怪。

注意: 如果你同时设置了 labelText 和 hintText,hintText 只有在 labelText 上浮后才会显示。所以一般建议二选一,或者用 labelText 就够了。

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,结果用户输入时光标总是跳到最前面,体验非常糟糕。后来我养成了习惯,每次格式化后都重新计算光标位置。

核心要点: 自定义 Formatter 时,一定要处理好 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 的整体管理和多字段联动,那才是真正体现水平的地方。