本文适合:刚入门 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 TextEditingControllerinitState中初始化 / 监听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_visibility、keyboard_actions等
5. 滥用 onChanged 做重型操作
onChanged触发频率非常高(每个字符输入 / 删除)- 不要在里面做复杂同步操作(比如每改一次就请求网络)
- 必须做的话,建议加防抖(debounce)