Flutter TextField 从入门到精通:掌握输入框的完整指南

本文适合:刚入门 Flutter 想搞懂 TextField 的同学,以及已经在项目中使用,但总觉得"只会 60%"的同学。

文章会从基础用法一直讲到 Controller、Focus、表单验证、输入限制、常见坑,配完整代码。

一、为什么要系统学 TextField?

在实际业务里,输入框几乎无处不在:

  • 登录 / 注册:手机号、验证码、密码
  • 搜索:搜索框 + 清空按钮 + 联想
  • 表单:收货地址、个人信息、反馈意见
  • 评论 / 聊天:多行输入 + 发送按钮

TextField 是 Flutter 中最基础的输入组件,但它涉及:

  • 状态管理(TextEditingController
  • 焦点管理(FocusNode
  • 装饰样式(InputDecoration + Theme
  • 验证与表单(TextFormField + Form
  • 输入限制(inputFormatters
  • 键盘行为与收起

如果这些你只是"零碎知道一点",那这篇可以帮你把脑子里的碎片拼成完整的知识图。

二、TextField 是什么?最小可用示例

1. TextField 是谁?

  • TextField:最基础的输入组件
  • TextFormField:在 Form 中使用的输入组件,天生支持表单验证(本质也是包了一层 TextField

最简单的 TextField

Dart 复制代码
class SimpleTextFieldDemo extends StatelessWidget {
  const SimpleTextFieldDemo({super.key});

  @override
  Widget build(BuildContext context) {
    return const Padding(
      padding: EdgeInsets.all(16),
      child: TextField(
        decoration: InputDecoration(
          border: OutlineInputBorder(),
          labelText: '用户名',
          hintText: '请输入用户名',
        ),
      ),
    );
  }
}

关键点:

  • 不指定 controller,内部也会维护一个
  • decoration 负责"长什么样"
  • 这是开发中最常见的写法之一

三、TextField 核心属性总览(按类别理解)

把属性按"功能"记,比一个个死记要舒服得多。

1. 文本内容相关

Dart 复制代码
TextField(
  controller: _controller,         // 文本控制器(推荐)
  onChanged: (value) {},           // 每次内容变化回调
  onSubmitted: (value) {},         // 点击键盘"完成/回车"时
)

controller:读写文本的入口

  • 读:_controller.text

  • 写:_controller.text = 'hello'

onChanged:每次字符变化都会触发(带中文输入法时,会比较频繁)

onSubmitted :用户点击键盘上的 done / send / search 时触发

注意:
TextField 没有 initialValue 属性(那是 TextFormField 的),想设置默认文本就写在 controller.text 里。

2. 光标和焦点相关

Dart 复制代码
TextField(
  focusNode: _focusNode,     // 控制焦点
  autofocus: true,           // 自动获取焦点
  enabled: true,             // 是否可编辑
  readOnly: false,           // 可获取焦点但不能改内容
  showCursor: true,          // 是否显示光标
  cursorColor: Colors.blue,  // 光标颜色
)
  • enabled = false:灰掉 + 不可编辑 + 不可获取焦点
  • readOnly = true:可以获取焦点、弹键盘(可控制),但内容不能改。很适合"点击输入框跳到新页面填写"的场景(比如"选择地址")

3. 键盘、输入行为相关

Dart 复制代码
TextField(
  keyboardType: TextInputType.number,   // 键盘类型:数字、email、多行...
  textInputAction: TextInputAction.done,// 键盘右下角按钮类型
  maxLength: 11,                        // 最大长度
  maxLines: 1,                          // 最大行数
  minLines: 1,                          // 最小行数
  inputFormatters: [                    // 输入限制
    FilteringTextInputFormatter.digitsOnly,
  ],
)

常见键盘类型:

  • TextInputType.text:普通文本
  • TextInputType.number:数字
  • TextInputType.phone:手机号
  • TextInputType.emailAddress:邮箱
  • TextInputType.multiline:支持换行

常见 textInputAction

  • TextInputAction.done:完成
  • TextInputAction.next:下一项(比如切换到下一个输入框)
  • TextInputAction.search:搜索
  • TextInputAction.send:发送

4. 文本样式相关

Dart 复制代码
TextField(
  style: const TextStyle(
    fontSize: 16,
    color: Colors.black87,
  ),
  textAlign: TextAlign.left,      // 对齐方式
  obscureText: true,              // 是否密文(密码)
  obscuringCharacter: '•',        // 密文替换字符
  maxLines: 1,                    // 单行
)

密码框的最简单写法:

Dart 复制代码
TextField(
  obscureText: true,
  decoration: const InputDecoration(
    labelText: '密码',
  ),
)

5. 装饰样式 InputDecoration(重点中的重点)

decoration 决定了 TextField 外观的 80%

Dart 复制代码
TextField(
  decoration: InputDecoration(
    labelText: '用户名',            // 上飘的标签
    hintText: '请输入用户名',      // 灰色提示
    helperText: '用户名长度 4~16',  // 底部辅助文案
    errorText: null,               // 错误提示
    prefixIcon: const Icon(Icons.person),   // 左侧图标
    suffixIcon: IconButton(                // 右侧图标
      icon: const Icon(Icons.clear),
      onPressed: () {},
    ),
    border: const OutlineInputBorder(),    // 默认边框
    focusedBorder: OutlineInputBorder(     // 聚焦边框
      borderSide: BorderSide(color: Colors.blue),
    ),
  ),
)

常见用法:

  • 登录 / 搜索:prefixIcon
  • 清空按钮 / 显示密码:suffixIcon
  • 错误提示:errorText: '手机号格式错误'

四、TextEditingController:输入框的"数据大脑"

1. 基本用法

Dart 复制代码
class ControllerDemo extends StatefulWidget {
  const ControllerDemo({super.key});

  @override
  State<ControllerDemo> createState() => _ControllerDemoState();
}

class _ControllerDemoState extends State<ControllerDemo> {
  final TextEditingController _controller = TextEditingController();

  @override
  void initState() {
    super.initState();
    _controller.text = '初始文本'; // 设置默认内容

    _controller.addListener(() {
      debugPrint('当前内容:${_controller.text}');
    });
  }

  @override
  void dispose() {
    _controller.dispose(); // 一定要释放
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        TextField(
          controller: _controller,
        ),
        ElevatedButton(
          onPressed: () {
            debugPrint('提交内容:${_controller.text}');
          },
          child: const Text('打印内容'),
        ),
      ],
    );
  }
}

要点:

  • State 里创建 final TextEditingController
  • initState 中初始化 / 监听
  • dispose 里记得 dispose(),避免内存泄漏

2. 控制光标位置 & 选中内容(进阶)

有时候你需要在中间插入文本、或者选中一段文本:

Dart 复制代码
_controller.value = _controller.value.copyWith(
  text: '新的内容',
  selection: TextSelection.collapsed(offset: '新的内容'.length),
);

常见场景:

  • 处理输入格式(比如自动插入空格)
  • 恢复光标位置

五、FocusNode:掌控焦点和键盘

1. FocusNode 的基本用法

Dart 复制代码
class FocusDemo extends StatefulWidget {
  const FocusDemo({super.key});

  @override
  State<FocusDemo> createState() => _FocusDemoState();
}

class _FocusDemoState extends State<FocusDemo> {
  final FocusNode _focusNode1 = FocusNode();
  final FocusNode _focusNode2 = FocusNode();

  @override
  void dispose() {
    _focusNode1.dispose();
    _focusNode2.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        TextField(
          focusNode: _focusNode1,
          decoration: const InputDecoration(labelText: '输入框1'),
        ),
        TextField(
          focusNode: _focusNode2,
          decoration: const InputDecoration(labelText: '输入框2'),
        ),
        ElevatedButton(
          onPressed: () {
            FocusScope.of(context).requestFocus(_focusNode2); // 切换到输入框2
          },
          child: const Text('切换到输入框2'),
        ),
      ],
    );
  }
}

2. 收起键盘(全局通用写法)

Dart 复制代码
void hideKeyboard(BuildContext context) {
  FocusScope.of(context).unfocus();
}

常见用法:

  • 页面点击空白处收起键盘
  • 提交后收起键盘

例子(外层包 GestureDetector):

Dart 复制代码
GestureDetector(
  onTap: () => FocusScope.of(context).unfocus(),
  behavior: HitTestBehavior.translucent,
  child: Scaffold(
    // ...
  ),
)

六、TextField vs TextFormField:什么时候用谁?

1. TextFormField 是谁?

  • 使用场景:需要表单验证
  • 搭配 Form + GlobalKey<FormState> 使用
  • 多个 TextFormField 可以统一 validate()save()

2. 登录表单示例(手机号 + 密码)

Dart 复制代码
class LoginFormDemo extends StatefulWidget {
  const LoginFormDemo({super.key});

  @override
  State<LoginFormDemo> createState() => _LoginFormDemoState();
}

class _LoginFormDemoState extends State<LoginFormDemo> {
  final GlobalKey<FormState> _formKey = GlobalKey<FormState>();

  String _phone = '';
  String _password = '';

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.all(16),
      child: Form(
        key: _formKey,
        child: Column(
          children: [
            TextFormField(
              decoration: const InputDecoration(
                labelText: '手机号',
                border: OutlineInputBorder(),
              ),
              keyboardType: TextInputType.phone,
              validator: (value) {
                if (value == null || value.isEmpty) {
                  return '请输入手机号';
                }
                if (value.length != 11) {
                  return '手机号长度必须为 11 位';
                }
                return null;
              },
              onSaved: (value) => _phone = value ?? '',
            ),
            const SizedBox(height: 16),
            TextFormField(
              decoration: const InputDecoration(
                labelText: '密码',
                border: OutlineInputBorder(),
              ),
              obscureText: true,
              validator: (value) {
                if (value == null || value.isEmpty) {
                  return '请输入密码';
                }
                if (value.length < 6) {
                  return '密码至少 6 位';
                }
                return null;
              },
              onSaved: (value) => _password = value ?? '',
            ),
            const SizedBox(height: 24),
            ElevatedButton(
              onPressed: () {
                if (_formKey.currentState?.validate() == true) {
                  _formKey.currentState?.save();
                  debugPrint('手机号=$_phone, 密码=$_password');
                }
              },
              child: const Text('登录'),
            ),
          ],
        ),
      ),
    );
  }
}

总结:

  • 不需要复杂验证 → 用 TextField 就够了

  • 多个输入框 + 统一验证 / 提交 → 推荐 TextFormField + Form

七、输入限制与格式化:inputFormatters 实战

inputFormatters 是一个 List<TextInputFormatter>,可以对每次输入做截断 / 替换。

1. 只允许数字输入

Dart 复制代码
TextField(
  keyboardType: TextInputType.number,
  inputFormatters: [
    FilteringTextInputFormatter.digitsOnly,
  ],
)

2. 小数(最多两位小数)

Dart 复制代码
class DecimalTextInputFormatter extends TextInputFormatter {
  final int decimalRange;

  DecimalTextInputFormatter({required this.decimalRange})
      : assert(decimalRange >= 0);

  @override
  TextEditingValue formatEditUpdate(
    TextEditingValue oldValue,
    TextEditingValue newValue,
  ) {
    String text = newValue.text;

    if (text.isEmpty) return newValue;

    // 非法字符
    if (!RegExp(r'^\d*\.?\d*$').hasMatch(text)) {
      return oldValue;
    }

    // 限制小数位
    if (text.contains('.') &&
        text.split('.').length == 2 &&
        text.split('.').last.length > decimalRange) {
      return oldValue;
    }

    return newValue;
  }
}

使用:

Dart 复制代码
TextField(
  keyboardType: const TextInputType.numberWithOptions(decimal: true),
  inputFormatters: [
    DecimalTextInputFormatter(decimalRange: 2),
  ],
)

3. 手机号中间自动插空格(进阶)

例如:138 0013 8000

思路:在 inputFormatter 中插入空格,并且恢复光标位置(略复杂,这里就不展开光标修正逻辑)。

八、三个常用"业务输入框"完整示例

1. 密码框 + 显示/隐藏眼睛按钮

Dart 复制代码
class PasswordField extends StatefulWidget {
  final TextEditingController controller;

  const PasswordField({super.key, required this.controller});

  @override
  State<PasswordField> createState() => _PasswordFieldState();
}

class _PasswordFieldState extends State<PasswordField> {
  bool _obscure = true;

  @override
  Widget build(BuildContext context) {
    return TextField(
      controller: widget.controller,
      obscureText: _obscure,
      decoration: InputDecoration(
        labelText: '密码',
        border: const OutlineInputBorder(),
        suffixIcon: IconButton(
          icon: Icon(_obscure ? Icons.visibility_off : Icons.visibility),
          onPressed: () {
            setState(() {
              _obscure = !_obscure;
            });
          },
        ),
      ),
    );
  }
}

2. 搜索框:带搜索图标 + 清空按钮 + 键盘搜索

Dart 复制代码
class SearchBar extends StatefulWidget {
  const SearchBar({super.key});

  @override
  State<SearchBar> createState() => _SearchBarState();
}

class _SearchBarState extends State<SearchBar> {
  final TextEditingController _controller = TextEditingController();

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

  void _onSearch(String keyword) {
    debugPrint('搜索:$keyword');
    // TODO: 调用搜索接口
  }

  @override
  Widget build(BuildContext context) {
    return TextField(
      controller: _controller,
      textInputAction: TextInputAction.search,
      onSubmitted: _onSearch,
      decoration: InputDecoration(
        hintText: '搜索内容',
        prefixIcon: const Icon(Icons.search),
        suffixIcon: _controller.text.isEmpty
            ? null
            : IconButton(
                icon: const Icon(Icons.clear),
                onPressed: () {
                  _controller.clear();
                  setState(() {});
                },
              ),
        border: OutlineInputBorder(
          borderRadius: BorderRadius.circular(24),
          borderSide: BorderSide.none,
        ),
        filled: true,
      ),
      onChanged: (value) {
        setState(() {}); // 刷新 suffixIcon 显隐
      },
    );
  }
}

3. 多行评论输入框 + 发送按钮

Dart 复制代码
class CommentInput extends StatefulWidget {
  const CommentInput({super.key});

  @override
  State<CommentInput> createState() => _CommentInputState();
}

class _CommentInputState extends State<CommentInput> {
  final TextEditingController _controller = TextEditingController();

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

  void _send() {
    final text = _controller.text.trim();
    if (text.isEmpty) return;
    debugPrint('发送评论:$text');
    _controller.clear();
    setState(() {});
  }

  @override
  Widget build(BuildContext context) {
    return Row(
      children: [
        Expanded(
          child: TextField(
            controller: _controller,
            minLines: 1,
            maxLines: 4,
            decoration: const InputDecoration(
              hintText: '写下你的评论...',
              border: OutlineInputBorder(),
            ),
            onChanged: (_) => setState(() {}),
          ),
        ),
        const SizedBox(width: 8),
        IconButton(
          icon: const Icon(Icons.send),
          onPressed: _controller.text.trim().isEmpty ? null : _send,
        ),
      ],
    );
  }
}

九、和键盘的交互:完成、下一项、收起

1. textInputAction + onEditingComplete

Dart 复制代码
TextField(
  textInputAction: TextInputAction.next,
  onEditingComplete: () {
    // 比如切到下一个 TextField
    FocusScope.of(context).nextFocus();
  },
)

常见组合:

  • 表单第一项:TextInputAction.next + nextFocus()

  • 最后一项:TextInputAction.done + unfocus() + 提交表单

2. 手动收起键盘的几种方式

1)最推荐:unfocus

Dart 复制代码
FocusScope.of(context).unfocus();

2)极端情况:调用系统通道隐藏

Dart 复制代码
SystemChannels.textInput.invokeMethod('TextInput.hide');

一般用不到,有 unfocus() 足够了。

十、国际化 / 中文输入法的一点注意

  • 中文输入法有"拼写中状态"(组合输入,尚未上屏)

  • onChanged 在拼写过程中会触发多次

  • 一些比较激进的 inputFormatter 会在组合状态下干扰输入

如果你对输入有复杂控制(比如在 onChanged 中强行改 controller.text),要注意不要破坏 IME 的组合状态 ,否则会出现"中文打不出来""光标乱跳"等问题。

实在复杂的场景,建议单独开一个 demo 专门调试各种输入法(包括 iOS / Android)。

十一、TextField 常见坑总结

1. 在 build() 里 new TextEditingController

❌ 错误写法:

Dart 复制代码
@override
Widget build(BuildContext context) {
  final controller = TextEditingController(); // 每次 build 都 new
  return TextField(controller: controller);
}

这样会导致:

  • 每次重建都重新 new controller,内容丢失

  • 还可能有内存泄漏

✅ 正确写法:

  • State 中声明为成员变量,并在 dispose() 里释放

2. 忘记 dispose() controller / focusNode

  • TextField 内部会订阅 controller 的变化

  • 如果不 dispose,页面关掉后仍然有监听 → 内存泄漏

3. TextField 外层没有高度约束

例如直接写在 Column 里且没有 Expanded / 父布局约束,可能会报错:

RenderFlex children have non-zero flex but incoming height constraints are unbounded

解决方式:

  • 给外层加 SizedBox / Expanded / Container(height: ...)
  • 或者正确使用 Column + mainAxisSize

4. 键盘遮挡输入框

常见场景:页面底部的输入框被键盘挡住了。

解决方向:

  • Scaffold 上:resizeToAvoidBottomInset: true(默认一般就是 true)
  • 外层使用可滚动布局,比如 SingleChildScrollView
  • 或者使用更高级的库:flutter_keyboard_visibilitykeyboard_actions

5. 滥用 onChanged 做重型操作

  • onChanged 触发频率非常高(每个字符输入 / 删除)
  • 不要在里面做复杂同步操作(比如每改一次就请求网络)
  • 必须做的话,建议加防抖(debounce)
相关推荐
wordbaby3 小时前
Flutter Form Builder 完全指南:告别 Controller 地狱
前端·flutter
tbit5 小时前
fluwx 拉起小程序WXLog:Error:fail to load Keychain status:-25300, keyData null:1
flutter·ios·微信小程序
QuantumLeap丶6 小时前
《Flutter全栈开发实战指南:从零到高级》- 19 -手势识别
flutter·ios·前端框架
卢叁8 小时前
Flutter之阿里云视频播放器支持 iOS模拟器解决方案
flutter
鹏多多11 小时前
flutter睡眠与冥想数据可视化神器:sleep_stage_chart插件全解析
android·前端·flutter
Zender Han18 小时前
Flutter 新版 Google Sign-In 插件完整解析(含示例讲解)
android·flutter·ios·web
weixin_4111918420 小时前
flutter中WebView的使用及JavaScript桥接的问题记录
javascript·flutter
QuantumLeap丶1 天前
《Flutter全栈开发实战指南:从零到高级》- 18 -自定义绘制与画布
android·flutter·ios
你听得到111 天前
Web前端们!我用三年亲身经历,说说从 uniapp 到 Flutter怎么转型的,这条路我爬过,坑我踩过
前端·flutter·uni-app