表单是 App 中最常见的交互场景之一。Flutter 提供了 Form + TextFormField 的声明式校验体系,配合 TextEditingController 和 FocusNode 实现完整的输入管理。
一、Form 与表单校验
1.1 基本表单
dart
class RegisterPage extends StatefulWidget {
@override
State<RegisterPage> createState() => _RegisterPageState();
}
class _RegisterPageState extends State<RegisterPage> {
final _formKey = GlobalKey<FormState>();
final _emailController = TextEditingController();
final _passwordController = TextEditingController();
final _confirmPasswordController = TextEditingController();
bool _obscurePassword = true;
@override
Widget build(BuildContext context) {
return Form(
key: _formKey,
autovalidateMode: AutovalidateMode.onUserInteraction, // 用户交互后自动校验
child: Column(
children: [
// 邮箱
TextFormField(
controller: _emailController,
keyboardType: TextInputType.emailAddress,
textInputAction: TextInputAction.next, // 键盘"下一个"按钮
decoration: const InputDecoration(
labelText: '邮箱',
hintText: 'example@mail.com',
prefixIcon: Icon(Icons.email_outlined),
),
validator: (value) {
if (value == null || value.isEmpty) return '请输入邮箱';
if (!RegExp(r'^[\w-\.]+@[\w-]+\.[a-zA-Z]{2,}$').hasMatch(value)) {
return '邮箱格式不正确';
}
return null; // null 表示校验通过
},
),
const SizedBox(height: 16),
// 密码
TextFormField(
controller: _passwordController,
obscureText: _obscurePassword,
textInputAction: TextInputAction.next,
decoration: InputDecoration(
labelText: '密码',
prefixIcon: const Icon(Icons.lock_outlined),
suffixIcon: IconButton(
icon: Icon(_obscurePassword
? Icons.visibility_off
: Icons.visibility),
onPressed: () =>
setState(() => _obscurePassword = !_obscurePassword),
),
),
validator: (value) {
if (value == null || value.length < 8) return '密码至少 8 位';
if (!RegExp(r'(?=.*[A-Z])(?=.*[0-9])').hasMatch(value)) {
return '必须包含大写字母和数字';
}
return null;
},
),
const SizedBox(height: 16),
// 确认密码
TextFormField(
controller: _confirmPasswordController,
obscureText: true,
textInputAction: TextInputAction.done,
decoration: const InputDecoration(
labelText: '确认密码',
prefixIcon: Icon(Icons.lock_outlined),
),
validator: (value) {
if (value != _passwordController.text) return '两次密码不一致';
return null;
},
),
const SizedBox(height: 24),
// 提交按钮
SizedBox(
width: double.infinity,
child: ElevatedButton(
onPressed: _submit,
child: const Text('注册'),
),
),
],
),
);
}
void _submit() {
if (_formKey.currentState!.validate()) {
// 所有校验通过
_formKey.currentState!.save(); // 触发 onSaved
_performRegister();
}
}
@override
void dispose() {
_emailController.dispose();
_passwordController.dispose();
_confirmPasswordController.dispose();
super.dispose();
}
}
二、TextEditingController 进阶
dart
class SearchField extends StatefulWidget { ... }
class _SearchFieldState extends State<SearchField> {
final _controller = TextEditingController();
@override
void initState() {
super.initState();
// 监听文本变化
_controller.addListener(() {
print('当前文本: ${_controller.text}');
print('光标位置: ${_controller.selection}');
});
}
void _insertAtCursor(String text) {
final cursorPos = _controller.selection.baseOffset;
final currentText = _controller.text;
_controller.text = currentText.substring(0, cursorPos) +
text +
currentText.substring(cursorPos);
// 移动光标到插入内容之后
_controller.selection = TextSelection.collapsed(
offset: cursorPos + text.length,
);
}
void _selectAll() {
_controller.selection = TextSelection(
baseOffset: 0,
extentOffset: _controller.text.length,
);
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
}
三、FocusNode(焦点管理)
dart
class LoginForm extends StatefulWidget { ... }
class _LoginFormState extends State<LoginForm> {
final _emailFocus = FocusNode();
final _passwordFocus = FocusNode();
@override
void initState() {
super.initState();
// 监听焦点变化
_emailFocus.addListener(() {
if (!_emailFocus.hasFocus) {
// 失去焦点时执行校验
_validateEmail();
}
});
}
@override
Widget build(BuildContext context) {
return Column(children: [
TextFormField(
focusNode: _emailFocus,
textInputAction: TextInputAction.next,
onFieldSubmitted: (_) {
// 按"下一个"时跳到密码框
FocusScope.of(context).requestFocus(_passwordFocus);
},
),
TextFormField(
focusNode: _passwordFocus,
textInputAction: TextInputAction.done,
onFieldSubmitted: (_) {
_passwordFocus.unfocus(); // 收起键盘
_submit();
},
),
// 点击空白区域收起键盘
]);
}
@override
void dispose() {
_emailFocus.dispose();
_passwordFocus.dispose();
super.dispose();
}
}
// 点击空白区域收起键盘(在 Scaffold 层处理)
GestureDetector(
onTap: () => FocusScope.of(context).unfocus(),
child: Scaffold(body: ...),
)
四、输入格式化(TextInputFormatter)
dart
import 'package:flutter/services.dart';
TextFormField(
keyboardType: TextInputType.number,
inputFormatters: [
FilteringTextInputFormatter.digitsOnly, // 仅允许数字
LengthLimitingTextInputFormatter(11), // 最多 11 位
// 或自定义格式化器
PhoneNumberFormatter(),
],
)
// 自定义手机号格式化器:138 0000 0000
class PhoneNumberFormatter extends TextInputFormatter {
@override
TextEditingValue formatEditUpdate(
TextEditingValue oldValue,
TextEditingValue newValue,
) {
final digits = newValue.text.replaceAll(RegExp(r'\D'), '');
final buffer = StringBuffer();
for (int i = 0; i < digits.length && i < 11; i++) {
if (i == 3 || i == 7) buffer.write(' ');
buffer.write(digits[i]);
}
final formatted = buffer.toString();
return TextEditingValue(
text: formatted,
selection: TextSelection.collapsed(offset: formatted.length),
);
}
}
// 金额输入:最多两位小数
TextFormField(
keyboardType: const TextInputType.numberWithOptions(decimal: true),
inputFormatters: [
FilteringTextInputFormatter.allow(RegExp(r'^\d+\.?\d{0,2}')),
],
)
// 身份证号
TextFormField(
inputFormatters: [
FilteringTextInputFormatter.allow(RegExp(r'[0-9Xx]')),
LengthLimitingTextInputFormatter(18),
],
)
五、键盘处理
dart
// 键盘弹出时自动滚动
Scaffold(
// resizeToAvoidBottomInset 默认 true,Scaffold 会自动缩减
resizeToAvoidBottomInset: true,
body: SingleChildScrollView(
// 确保输入框在键盘弹出时可见
reverse: false,
child: Padding(
padding: EdgeInsets.only(
bottom: MediaQuery.of(context).viewInsets.bottom,
),
child: _buildForm(),
),
),
)
// 监听键盘高度变化
Widget build(BuildContext context) {
final keyboardHeight = MediaQuery.of(context).viewInsets.bottom;
final isKeyboardVisible = keyboardHeight > 0;
return AnimatedPadding(
duration: const Duration(milliseconds: 200),
padding: EdgeInsets.only(bottom: isKeyboardVisible ? keyboardHeight : 0),
child: _buildContent(),
);
}
六、搜索防抖(Debounce)
dart
class SearchPage extends StatefulWidget { ... }
class _SearchPageState extends State<SearchPage> {
final _controller = TextEditingController();
Timer? _debounceTimer;
List<Product> _results = [];
void _onSearchChanged(String query) {
_debounceTimer?.cancel(); // 取消前一次
_debounceTimer = Timer(
const Duration(milliseconds: 500), // 500ms 防抖
() => _performSearch(query),
);
}
Future<void> _performSearch(String query) async {
if (query.trim().isEmpty) {
setState(() => _results = []);
return;
}
final results = await SearchService.search(query);
if (mounted) setState(() => _results = results);
}
@override
Widget build(BuildContext context) {
return Column(children: [
TextField(
controller: _controller,
onChanged: _onSearchChanged,
decoration: InputDecoration(
hintText: '搜索商品...',
prefixIcon: const Icon(Icons.search),
suffixIcon: _controller.text.isNotEmpty
? IconButton(
icon: const Icon(Icons.clear),
onPressed: () {
_controller.clear();
_onSearchChanged('');
},
)
: null,
),
),
Expanded(child: SearchResultList(results: _results)),
]);
}
@override
void dispose() {
_debounceTimer?.cancel();
_controller.dispose();
super.dispose();
}
}
小结
| 功能 | 核心 API |
|---|---|
| 表单校验 | Form + GlobalKey<FormState> + validator |
| 文本控制 | TextEditingController(读取/设置文本、光标位置) |
| 焦点管理 | FocusNode + FocusScope(跳转/收起键盘) |
| 输入格式化 | TextInputFormatter(数字/手机号/金额) |
| 键盘适配 | MediaQuery.viewInsets.bottom |
| 搜索防抖 | Timer + 500ms 延迟 |
👉 下一节:2.7 列表与滚动性能优化