3.5 输入框及表单
📚 章节概览
输入框和表单是用户交互的核心组件。Flutter 提供了强大的输入处理能力,本章节将学习:
- TextField - 基本输入框
- TextEditingController - 控制器
- FocusNode - 焦点管理
- InputDecoration - 装饰器
- 键盘类型 - keyboardType
- Form - 表单组件
- TextFormField - 表单输入框
- 表单验证 - validator
🎯 核心知识点
1. TextField
TextField 是 Flutter 中用于文本输入的基本组件。
基本用法
TextField(
decoration: InputDecoration(
labelText: '用户名',
hintText: '请输入用户名',
),
)
常用属性
| 属性 | 类型 | 说明 |
|---|---|---|
controller | TextEditingController? | 控制器 |
focusNode | FocusNode? | 焦点节点 |
decoration | InputDecoration? | 装饰器 |
keyboardType | TextInputType? | 键盘类型 |
textInputAction | TextInputAction? | 键盘动作按钮 |
style | TextStyle? | 文本样式 |
textAlign | TextAlign | 文本对齐 |
autofocus | bool | 自动获取焦点 |
obscureText | bool | 隐藏文本(密码) |
maxLines | int? | 最大行数 |
maxLength | int? | 最大长度 |
enabled | bool? | 是否启用 |
onChanged | ValueChanged? | 内容改变回调 |
onSubmitted | ValueChanged? | 提交回调 |
inputFormatters | List? | 输入格式化器 |
2️⃣ TextEditingController
TextEditingController 用于控制TextField的文本内容。
基本用法
class MyWidget extends StatefulWidget {
@override
_MyWidgetState createState() => _MyWidgetState();
}
class _MyWidgetState extends State<MyWidget> {
final TextEditingController _controller = TextEditingController();
@override
void initState() {
super.initState();
// 设置初始值
_controller.text = 'Hello';
// 监听输入变化
_controller.addListener(() {
print('当前输入:${_controller.text}');
});
}
@override
void dispose() {
// 释放控制器
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return TextField(
controller: _controller,
);
}
}
常用方法
| 方法 | 说明 |
|---|---|
text | 获取/设置文本内容 |
clear() | 清空文本 |
selection | 获取/设置选中文本 |
addListener() | 添加监听器 |
removeListener() | 移除监听器 |
dispose() | 释放资源 |
选中文本
// 全选
_controller.selection = TextSelection(
baseOffset: 0,
extentOffset: _controller.text.length,
);
// 选中部分文本
_controller.selection = TextSelection(
baseOffset: 0,
extentOffset: 5,
);
3️⃣ FocusNode - 焦点管理
FocusNode 用于控制 TextField 的焦点状态。
基本用法
class FocusExample extends StatefulWidget {
@override
_FocusExampleState createState() => _FocusExampleState();
}
class _FocusExampleState extends State<FocusExample> {
final FocusNode _focusNode = FocusNode();
@override
void initState() {
super.initState();
// 监听焦点变化
_focusNode.addListener(() {
print('焦点状态:${_focusNode.hasFocus}');
});
}
@override
void dispose() {
_focusNode.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Column(
children: [
TextField(
focusNode: _focusNode,
decoration: InputDecoration(labelText: '输入框'),
),
ElevatedButton(
onPressed: () {
// 请求焦点
FocusScope.of(context).requestFocus(_focusNode);
},
child: Text('获取焦点'),
),
ElevatedButton(
onPressed: () {
// 失去焦点
_focusNode.unfocus();
},
child: Text('失去焦点'),
),
],
);
}
}
焦点操作
| 方法 | 说明 |
|---|---|
requestFocus() | 请求焦点 |
unfocus() | 失去焦点 |
hasFocus | 是否有焦点 |
nextFocus() | 移到下一个焦点 |
previousFocus() | 移到上一个焦点 |
4️⃣ InputDecoration - 装饰器
InputDecoration 用于装饰 TextField 的外观。
完整示例
TextField(
decoration: InputDecoration(
// 标签
labelText: '用户名',
labelStyle: TextStyle(color: Colors.blue),
// 提示文本
hintText: '请输入用户名',
hintStyle: TextStyle(color: Colors.grey),
// 帮助文本
helperText: '用于登录的用户名',
// 错误提示
errorText: '用户名不能为空',
// 图标
icon: Icon(Icons.person), // 输入框外的图标
prefixIcon: Icon(Icons.person), // 输入框内左侧图标
suffixIcon: Icon(Icons.clear), // 输入框内右侧图标
// 前缀/后缀文本
prefix: Text('Mr.'),
suffix: Text('@gmail.com'),
// 背景填充
filled: true,
fillColor: Colors.grey[200],
// 边框
border: OutlineInputBorder(),
enabledBorder: OutlineInputBorder(
borderSide: BorderSide(color: Colors.grey),
),
focusedBorder: OutlineInputBorder(
borderSide: BorderSide(color: Colors.blue, width: 2),
),
errorBorder: OutlineInputBorder(
borderSide: BorderSide(color: Colors.red),
),
),
)
边框样式
// 1. 默认边框(下划线)
InputDecoration(
border: UnderlineInputBorder(),
)
// 2. 轮廓边框
InputDecoration(
border: OutlineInputBorder(),
)
// 3. 圆角边框
InputDecoration(
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
),
)
// 4. 无边框
InputDecoration(
border: InputBorder.none,
)
5️⃣ 键盘类型
通过 keyboardType 设置不同的键盘类型。
| 类型 | 说明 | 键盘显示 |
|---|---|---|
TextInputType.text | 默认文本 | 标准键盘 |
TextInputType.number | 数字 | 数字键盘 |
TextInputType.phone | 电话 | 电话号码键盘 |
TextInputType.emailAddress | 邮箱 | 带@的键盘 |
TextInputType.url | 网址 | 带/的键盘 |
TextInputType.datetime | 日期时间 | 带-和:的键盘 |
TextInputType.multiline | 多行 | 支持换行 |
示例
// 数字键盘
TextField(
keyboardType: TextInputType.number,
decoration: InputDecoration(labelText: '年龄'),
)
// 邮箱键盘
TextField(
keyboardType: TextInputType.emailAddress,
decoration: InputDecoration(labelText: '邮箱'),
)
// 多行输入
TextField(
keyboardType: TextInputType.multiline,
maxLines: 3,
decoration: InputDecoration(labelText: '备注'),
)
6️⃣ 输入格式化
使用 inputFormatters 限制输入内容。
常用格式化器
// 1. 只允许数字
TextField(
inputFormatters: [FilteringTextInputFormatter.digitsOnly],
)
// 2. 限制长度
TextField(
inputFormatters: [LengthLimitingTextInputFormatter(10)],
)
// 3. 自定义正则
TextField(
inputFormatters: [
FilteringTextInputFormatter.allow(RegExp(r'[a-zA-Z0-9]')),
],
)
// 4. 组合多个格式化器
TextField(
inputFormatters: [
FilteringTextInputFormatter.digitsOnly,
LengthLimitingTextInputFormatter(11),
],
)
7️⃣ Form - 表单
Form 组件用于表单验证和统一管理。
基本用法
class MyForm extends StatefulWidget {
@override
_MyFormState createState() => _MyFormState();
}
class _MyFormState extends State<MyForm> {
final GlobalKey<FormState> _formKey = GlobalKey<FormState>();
@override
Widget build(BuildContext context) {
return Form(
key: _formKey,
child: Column(
children: [
TextFormField(
decoration: InputDecoration(labelText: '用户名'),
validator: (value) {
if (value == null || value.isEmpty) {
return '请输入用户名';
}
return null;
},
),
ElevatedButton(
onPressed: () {
if (_formKey.currentState!.validate()) {
// 验证通过
print('表单验证通过');
}
},
child: Text('提交'),
),
],
),
);
}
}
Form 属性
| 属性 | 类型 | 说明 |
|---|---|---|
key | GlobalKey | 全局键 |
autovalidateMode | AutovalidateMode | 自动验证模式 |
onChanged | VoidCallback? | 内容改变回调 |
AutovalidateMode
// 不自动验证(默认)
Form(
autovalidateMode: AutovalidateMode.disabled,
)
// 用户交互后验证
Form(
autovalidateMode: AutovalidateMode.onUserInteraction,
)
// 始终验证
Form(
autovalidateMode: AutovalidateMode.always,
)
8️⃣ TextFormField
TextFormField 是 TextField 的Form版本,支持验证。
基本用法
TextFormField(
decoration: InputDecoration(labelText: '用户名'),
validator: (value) {
if (value == null || value.isEmpty) {
return '用户名不能为空';
}
if (value.length < 3) {
return '用户名至少3个字符';
}
return null; // 验证通过返回null
},
onSaved: (value) {
// 保存数据
print('保存:$value');
},
)
验证器返回值
null:验证通过String:验证失败,返回错误提示
9️⃣ FormState
通过 GlobalKey 获取 FormState 进行操作。
常用方法
| 方法 | 说明 |
|---|---|
validate() | 验证所有字段 |
save() | 保存所有字段 |
reset() | 重置所有字段 |
完整示例
class LoginForm extends StatefulWidget {
@override
_LoginFormState createState() => _LoginFormState();
}
class _LoginFormState extends State<LoginForm> {
final GlobalKey<FormState> _formKey = GlobalKey<FormState>();
final TextEditingController _usernameController = TextEditingController();
final TextEditingController _passwordController = TextEditingController();
@override
void dispose() {
_usernameController.dispose();
_passwordController.dispose();
super.dispose();
}
void _submitForm() {
// 1. 验证
if (_formKey.currentState!.validate()) {
// 2. 保存
_formKey.currentState!.save();
// 3. 提交数据
print('用户名:${_usernameController.text}');
print('密码:${_passwordController.text}');
}
}
void _resetForm() {
// 重置表单
_formKey.currentState!.reset();
_usernameController.clear();
_passwordController.clear();
}
@override
Widget build(BuildContext context) {
return Form(
key: _formKey,
autovalidateMode: AutovalidateMode.onUserInteraction,
child: Column(
children: [
TextFormField(
controller: _usernameController,
decoration: InputDecoration(
labelText: '用户名',
prefixIcon: Icon(Icons.person),
),
validator: (value) {
if (value == null || value.trim().isEmpty) {
return '用户名不能为空';
}
return null;
},
),
TextFormField(
controller: _passwordController,
decoration: InputDecoration(
labelText: '密码',
prefixIcon: Icon(Icons.lock),
),
obscureText: true,
validator: (value) {
if (value == null || value.trim().isEmpty) {
return '密码不能为空';
}
if (value.length < 6) {
return '密码不能少于6位';
}
return null;
},
),
Row(
children: [
ElevatedButton(
onPressed: _resetForm,
child: Text('重置'),
),
ElevatedButton(
onPressed: _submitForm,
child: Text('提交'),
),
],
),
],
),
);
}
}
💡 最佳实践
1. 总是释放控制器
@override
void dispose() {
_controller.dispose();
_focusNode.dispose();
super.dispose();
}
2. 使用 GlobalKey 获取 FormState
final GlobalKey<FormState> _formKey = GlobalKey<FormState>();
// 不推荐:通过 context 获取(可能找不到)
Form.of(context).validate();
// 推荐:通过 GlobalKey 获取
_formKey.currentState!.validate();
3. 自动验证模式
// 推荐:用户交互后才验证
Form(
autovalidateMode: AutovalidateMode.onUserInteraction,
)
// 不推荐:始终验证(用户还没输入就显示错误)
Form(
autovalidateMode: AutovalidateMode.always,
)
4. 监听输入变化
// 方法1:使用 controller.addListener
_controller.addListener(() {
print(_controller.text);
});
// 方法2:使用 onChanged
TextField(
onChanged: (value) {
print(value);
},
)
5. 常见验证规则
// 邮箱验证
String? validateEmail(String? value) {
if (value == null || value.isEmpty) {
return '邮箱不能为空';
}
final emailRegex = RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$');
if (!emailRegex.hasMatch(value)) {
return '请输入有效的邮箱地址';
}
return null;
}
// 手机号验证
String? validatePhone(String? value) {
if (value == null || value.isEmpty) {
return '手机号不能为空';
}
final phoneRegex = RegExp(r'^1[3-9]\d{9}$');
if (!phoneRegex.hasMatch(value)) {
return '请输入有效的手机号';
}
return null;
}
// 密码强度验证
String? validatePassword(String? value) {
if (value == null || value.isEmpty) {
return '密码不能为空';
}
if (value.length < 8) {
return '密码不能少于8位';
}
if (!RegExp(r'[A-Z]').hasMatch(value)) {
return '密码必须包含大写字母';
}
if (!RegExp(r'[a-z]').hasMatch(value)) {
return '密码必须包含小写字母';
}
if (!RegExp(r'[0-9]').hasMatch(value)) {
return '密码必须包含数字';
}
return null;
}
🤔 常见问题(FAQ)
Q1: TextField 和 TextFormField 有什么区别?
A:
| 特性 | TextField | TextFormField |
|---|---|---|
| 用途 | 基础输入框 | 表单输入框 |
| 验证 | 需要手动实现 | 内置 validator |
| 使用场景 | 单独使用 | 配合 Form 使用 |
| 继承关系 | StatefulWidget | FormField |
// TextField:手动验证
TextField(
onChanged: (value) {
if (value.isEmpty) {
// 手动显示错误
}
},
)
// TextFormField:自动验证
TextFormField(
validator: (value) {
if (value?.isEmpty ?? true) {
return '不能为空'; // 自动显示错误
}
return null;
},
)
Q2: 如何实现"显示/隐藏密码"功能?
A:
class PasswordField extends StatefulWidget {
@override
_PasswordFieldState createState() => _PasswordFieldState();
}
class _PasswordFieldState extends State<PasswordField> {
bool _obscureText = true;
@override
Widget build(BuildContext context) {
return TextField(
obscureText: _obscureText,
decoration: InputDecoration(
labelText: '密码',
suffixIcon: IconButton(
icon: Icon(
_obscureText ? Icons.visibility : Icons.visibility_off,
),
onPressed: () {
setState(() {
_obscureText = !_obscureText;
});
},
),
),
);
}
}
Q3: 如何限制只能输入数字?
A: 三种方法:
// 方法1:设置数字键盘
TextField(
keyboardType: TextInputType.number,
)
// 方法2:使用 inputFormatters
TextField(
inputFormatters: [FilteringTextInputFormatter.digitsOnly],
)
// 方法3:在 validator 中检查
TextFormField(
validator: (value) {
if (value != null && !RegExp(r'^\d+$').hasMatch(value)) {
return '只能输入数字';
}
return null;
},
)
Q4: 如何实现"确认密码"验证?
A:
class PasswordForm extends StatefulWidget {
@override
_PasswordFormState createState() => _PasswordFormState();
}
class _PasswordFormState extends State<PasswordForm> {
final TextEditingController _passwordController = TextEditingController();
@override
Widget build(BuildContext context) {
return Column(
children: [
TextFormField(
controller: _passwordController,
decoration: InputDecoration(labelText: '密码'),
obscureText: true,
validator: (value) {
if (value == null || value.length < 6) {
return '密码不能少于6位';
}
return null;
},
),
TextFormField(
decoration: InputDecoration(labelText: '确认密码'),
obscureText: true,
validator: (value) {
if (value != _passwordController.text) {
return '两次密码输入不一致';
}
return null;
},
),
],
);
}
}
Q5: 如何实现实时搜索?
A:
class SearchField extends StatefulWidget {
@override
_SearchFieldState createState() => _SearchFieldState();
}
class _SearchFieldState extends State<SearchField> {
final TextEditingController _controller = TextEditingController();
Timer? _debounce;
@override
void initState() {
super.initState();
_controller.addListener(_onSearchChanged);
}
@override
void dispose() {
_controller.dispose();
_debounce?.cancel();
super.dispose();
}
void _onSearchChanged() {
// 防抖:用户停止输入500ms后才执行搜索
if (_debounce?.isActive ?? false) _debounce!.cancel();
_debounce = Timer(const Duration(milliseconds: 500), () {
// 执行搜索
print('搜索:${_controller.text}');
});
}
@override
Widget build(BuildContext context) {
return TextField(
controller: _controller,
decoration: InputDecoration(
labelText: '搜索',
prefixIcon: Icon(Icons.search),
suffixIcon: IconButton(
icon: Icon(Icons.clear),
onPressed: () {
_controller.clear();
},
),
),
);
}
}
🎯 跟着做练习
练习1:实现一个注册表单
目标: 创建注册表单,包含用户名、邮箱、密码、确认密码
步骤:
- 使用
Form和TextFormField - 实现表单验证
- 提交时显示输入内容
💡 查看答案
class RegisterForm extends StatefulWidget {
const RegisterForm({super.key});
@override
State<RegisterForm> createState() => _RegisterFormState();
}
class _RegisterFormState extends State<RegisterForm> {
final GlobalKey<FormState> _formKey = GlobalKey<FormState>();
final TextEditingController _usernameController = TextEditingController();
final TextEditingController _emailController = TextEditingController();
final TextEditingController _passwordController = TextEditingController();
@override
void dispose() {
_usernameController.dispose();
_emailController.dispose();
_passwordController.dispose();
super.dispose();
}
void _submitForm() {
if (_formKey.currentState!.validate()) {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('注册信息'),
content: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('用户名:${_usernameController.text}'),
Text('邮箱:${_emailController.text}'),
],
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('确定'),
),
],
),
);
}
}
@override
Widget build(BuildContext context) {
return Form(
key: _formKey,
autovalidateMode: AutovalidateMode.onUserInteraction,
child: ListView(
padding: const EdgeInsets.all(16),
children: [
TextFormField(
controller: _usernameController,
decoration: const InputDecoration(
labelText: '用户名',
hintText: '请输入用户名',
prefixIcon: Icon(Icons.person),
border: OutlineInputBorder(),
),
validator: (value) {
if (value == null || value.trim().isEmpty) {
return '用户名不能为空';
}
if (value.length < 3) {
return '用户名至少3个字符';
}
return null;
},
),
const SizedBox(height: 16),
TextFormField(
controller: _emailController,
keyboardType: TextInputType.emailAddress,
decoration: const InputDecoration(
labelText: '邮箱',
hintText: '请输入邮箱',
prefixIcon: Icon(Icons.email),
border: OutlineInputBorder(),
),
validator: (value) {
if (value == null || value.trim().isEmpty) {
return '邮箱不能为空';
}
final emailRegex = RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$');
if (!emailRegex.hasMatch(value)) {
return '请输入有效的邮箱地址';
}
return null;
},
),
const SizedBox(height: 16),
TextFormField(
controller: _passwordController,
obscureText: true,
decoration: const InputDecoration(
labelText: '密码',
hintText: '请输入密码',
prefixIcon: Icon(Icons.lock),
border: OutlineInputBorder(),
),
validator: (value) {
if (value == null || value.trim().isEmpty) {
return '密码不能为空';
}
if (value.length < 6) {
return '密码不能少于6位';
}
return null;
},
),
const SizedBox(height: 16),
TextFormField(
obscureText: true,
decoration: const InputDecoration(
labelText: '确认密码',
hintText: '请再次输入密码',
prefixIcon: Icon(Icons.lock_outline),
border: OutlineInputBorder(),
),
validator: (value) {
if (value != _passwordController.text) {
return '两次密码输入不一致';
}
return null;
},
),
const SizedBox(height: 24),
ElevatedButton(
onPressed: _submitForm,
child: const Padding(
padding: EdgeInsets.all(12),
child: Text('注册', style: TextStyle(fontSize: 16)),
),
),
],
),
);
}
}
练习2:实现一个搜索框
目标: 实现带清空按钮和实时搜索的搜索框
步骤:
- 使用
TextEditingController - 添加清空按钮
- 实现防抖搜索
💡 查看答案
import 'dart:async';
class SearchBox extends StatefulWidget {
const SearchBox({super.key});
@override
State<SearchBox> createState() => _SearchBoxState();
}
class _SearchBoxState extends State<SearchBox> {
final TextEditingController _controller = TextEditingController();
Timer? _debounce;
List<String> _results = [];
bool _isSearching = false;
@override
void initState() {
super.initState();
_controller.addListener(_onSearchChanged);
}
@override
void dispose() {
_controller.dispose();
_debounce?.cancel();
super.dispose();
}
void _onSearchChanged() {
if (_debounce?.isActive ?? false) _debounce!.cancel();
setState(() {
_isSearching = true;
});
_debounce = Timer(const Duration(milliseconds: 500), () {
// 模拟搜索
_performSearch(_controller.text);
});
}
void _performSearch(String query) {
if (query.isEmpty) {
setState(() {
_results = [];
_isSearching = false;
});
return;
}
// 模拟搜索结果
final allItems = [
'Flutter',
'Dart',
'Widget',
'StatelessWidget',
'StatefulWidget',
'Material',
'Cupertino',
];
setState(() {
_results = allItems
.where((item) => item.toLowerCase().contains(query.toLowerCase()))
.toList();
_isSearching = false;
});
}
@override
Widget build(BuildContext context) {
return Column(
children: [
TextField(
controller: _controller,
decoration: InputDecoration(
labelText: '搜索',
hintText: '输入关键词搜索',
prefixIcon: const Icon(Icons.search),
suffixIcon: _controller.text.isNotEmpty
? IconButton(
icon: const Icon(Icons.clear),
onPressed: () {
_controller.clear();
},
)
: null,
border: const OutlineInputBorder(),
),
),
const SizedBox(height: 16),
if (_isSearching)
const CircularProgressIndicator()
else if (_results.isNotEmpty)
...results.map((result) => ListTile(
title: Text(result),
onTap: () {
print('选中:$result');
},
))
else if (_controller.text.isNotEmpty)
const Text('无搜索结果'),
],
);
}
}
📋 小结
核心要点
| 组件 | 用途 | 关键特性 |
|---|---|---|
| TextField | 基础输入框 | controller, decoration, keyboardType |
| TextEditingController | 控制器 | text, selection, addListener |
| FocusNode | 焦点管理 | hasFocus, requestFocus, unfocus |
| InputDecoration | 装饰器 | labelText, hintText, prefixIcon, border |
| Form | 表单 | validate, save, reset |
| TextFormField | 表单输入框 | validator, onSaved |
记忆技巧
- 控制器三步曲:创建 → 使用 → 释放
- 焦点管理:FocusNode + FocusScope
- 表单验证:Form + TextFormField + GlobalKey
- 验证器返回值:null = 通过,String = 错误信息