在移动应用开发中,表单是收集用户输入的重要方式,无论是登录注册、个人信息填写还是数据提交,都离不开表单组件。Flutter 提供了一套完整的表单处理机制,包括表单组件、输入验证、数据处理等功能。本节课将详细介绍 Flutter 中的表单与输入相关组件,帮助你构建功能完善、用户体验良好的表单界面。
一、表单基础组件
Flutter 提供了 Form
组件作为表单容器,配合各种输入控件(如 TextFormField
)实现完整的表单功能。Form
组件本身不会渲染任何可见内容,它主要用于管理表单字段的状态、验证和提交。
1. Form 组件与 GlobalKey
Form
组件需要通过 GlobalKey<FormState>
来管理表单状态,实现表单验证和数据提交等操作。GlobalKey
是跨组件访问状态的一种方式,能够唯一标识一个组件并获取其状态。
基本用法示例:
dart
class BasicFormExample extends StatefulWidget {
const BasicFormExample({super.key});
@override
State<BasicFormExample> createState() => _BasicFormExampleState();
}
class _BasicFormExampleState extends State<BasicFormExample> {
// 创建表单全局键
final _formKey = GlobalKey<FormState>();
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Basic Form')),
body: Padding(
padding: const EdgeInsets.all(16.0),
// 表单组件
child: Form(
key: _formKey, // 关联全局键
autovalidateMode: AutovalidateMode.onUserInteraction, // 验证模式
child: Column(
children: [
// 表单字段
TextFormField(
decoration: const InputDecoration(
labelText: 'Username',
border: OutlineInputBorder(),
),
// 验证器
validator: (value) {
if (value == null || value.isEmpty) {
return 'Please enter your username';
}
return null; // 验证通过
},
),
const SizedBox(height: 16),
ElevatedButton(
onPressed: () {
// 验证表单
if (_formKey.currentState!.validate()) {
// 验证通过,处理数据
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Processing data')),
);
}
},
child: const Text('Submit'),
),
],
),
),
),
);
}
}
Form
组件的核心参数:
-
key
:用于访问表单状态的GlobalKey<FormState>
-
autovalidateMode
:自动验证模式,决定何时自动验证输入AutovalidateMode.disabled
:禁用自动验证(默认)AutovalidateMode.always
:总是自动验证AutovalidateMode.onUserInteraction
:用户交互时(如输入、焦点变化)验证
2. TextFormField 组件
TextFormField
是表单中最常用的输入组件,继承自 TextField
并增加了表单验证功能。它支持各种输入类型(文本、数字、邮箱等),并可自定义外观和行为。
常用属性示例:
dart
TextFormField(
// 输入框装饰
decoration: InputDecoration(
labelText: 'Email',
hintText: 'Enter your email address',
prefixIcon: const Icon(Icons.email),
border: const OutlineInputBorder(),
// 错误提示样式
errorBorder: OutlineInputBorder(
borderSide: const BorderSide(color: Colors.red),
borderRadius: BorderRadius.circular(4),
),
),
// 输入类型
keyboardType: TextInputType.emailAddress,
// 输入格式器(可限制输入内容)
inputFormatters: [
FilteringTextInputFormatter.deny(RegExp(r'\s')), // 禁止输入空格
],
// 文本大小写转换
textCapitalization: TextCapitalization.none,
// 密码隐藏
obscureText: false, // 密码框设为true
// 自动更正
autocorrect: false,
// 自动获取焦点
autofocus: false,
// 最大长度
maxLength: 50,
// 最大行数
maxLines: 1,
// 验证器
validator: (value) {
if (value == null || value.isEmpty) {
return 'Please enter your email';
}
// 简单的邮箱格式验证
if (!RegExp(r'^[\w-.]+@([\w-]+.)+[\w-]{2,4}$').hasMatch(value)) {
return 'Please enter a valid email';
}
return null;
},
// 输入变化回调
onChanged: (value) {
print('Email changed: $value');
},
// 完成输入回调
onFieldSubmitted: (value) {
print('Email submitted: $value');
},
)
3. 其他输入组件
除了文本输入,Flutter 还提供了其他常用的表单输入组件:
- Checkbox:复选框,用于选择多个选项
- Radio:单选按钮,用于从多个选项中选择一个
- Switch:开关,用于开启 / 关闭某个功能
- DropdownButton:下拉选择框,用于从预设选项中选择
这些组件可以通过 FormField
包装以集成到表单中:
dart
// 复选框表单字段
FormField<bool>(
initialValue: false,
validator: (value) {
if (value == false) {
return 'Please agree to the terms';
}
return null;
},
builder: (formFieldState) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Checkbox(
value: formFieldState.value,
onChanged: (value) {
formFieldState.didChange(value);
},
),
const Text('I agree to the terms and conditions'),
],
),
if (formFieldState.hasError)
Padding(
padding: const EdgeInsets.only(top: 4, left: 4),
child: Text(
formFieldState.errorText!,
style: const TextStyle(color: Colors.red, fontSize: 12),
),
),
],
);
},
)
// 下拉选择框
FormField<String>(
initialValue: 'male',
validator: (value) {
if (value == null || value.isEmpty) {
return 'Please select gender';
}
return null;
},
builder: (formFieldState) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
DropdownButton<String>(
value: formFieldState.value,
isExpanded: true,
items: const [
DropdownMenuItem(value: 'male', child: Text('Male')),
DropdownMenuItem(value: 'female', child: Text('Female')),
DropdownMenuItem(value: 'other', child: Text('Other')),
],
onChanged: (value) {
formFieldState.didChange(value);
},
),
if (formFieldState.hasError)
Padding(
padding: const EdgeInsets.only(top: 4, left: 4),
child: Text(
formFieldState.errorText!,
style: const TextStyle(color: Colors.red, fontSize: 12),
),
),
],
);
},
)
二、输入验证与表单提交
表单验证是确保用户输入符合要求的重要环节,Flutter 提供了灵活的验证机制,可实现简单到复杂的各种验证逻辑。
1. 基本验证逻辑
每个 TextFormField
都可以通过 validator
属性指定验证函数,该函数接收输入值并返回错误信息(验证失败)或 null
(验证成功)。
常见验证示例:
dart
// 用户名验证
validator: (value) {
if (value == null || value.isEmpty) {
return 'Please enter username';
}
if (value.length < 3) {
return 'Username must be at least 3 characters';
}
if (value.length > 20) {
return 'Username cannot exceed 20 characters';
}
return null;
}
// 密码验证
validator: (value) {
if (value == null || value.isEmpty) {
return 'Please enter password';
}
if (value.length < 6) {
return 'Password must be at least 6 characters';
}
if (!RegExp(r'[A-Z]').hasMatch(value)) {
return 'Password must contain at least one uppercase letter';
}
return null;
}
// 确认密码验证(需要与密码字段比较)
validator: (value) {
if (value == null || value.isEmpty) {
return 'Please confirm password';
}
if (value != _passwordController.text) {
return 'Passwords do not match';
}
return null;
}
2. 表单提交与数据处理
通过 GlobalKey<FormState>
可以触发表单验证和获取表单数据:
dart
class FormSubmissionExample extends StatefulWidget {
const FormSubmissionExample({super.key});
@override
State<FormSubmissionExample> createState() => _FormSubmissionExampleState();
}
class _FormSubmissionExampleState extends State<FormSubmissionExample> {
final _formKey = GlobalKey<FormState>();
final _usernameController = TextEditingController();
final _emailController = TextEditingController();
@override
void dispose() {
// 释放控制器资源
_usernameController.dispose();
_emailController.dispose();
super.dispose();
}
// 提交表单
void _submitForm() {
// 验证所有字段
if (_formKey.currentState!.validate()) {
// 验证通过,保存表单状态
_formKey.currentState!.save();
// 获取输入值
final username = _usernameController.text;
final email = _emailController.text;
// 处理表单数据(如提交到服务器)
_processFormData(username, email);
}
}
// 处理表单数据
void _processFormData(String username, String email) {
print('Username: $username');
print('Email: $email');
// 显示提交成功消息
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Form submitted successfully')),
);
// 可以在这里导航到其他页面
// Navigator.pushNamed(context, '/success');
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Form Submission')),
body: Padding(
padding: const EdgeInsets.all(16.0),
child: Form(
key: _formKey,
autovalidateMode: AutovalidateMode.onUserInteraction,
child: Column(
children: [
TextFormField(
controller: _usernameController,
decoration: const InputDecoration(
labelText: 'Username',
border: OutlineInputBorder(),
),
validator: (value) {
if (value == null || value.isEmpty) {
return 'Please enter username';
}
if (value.length < 3) {
return 'Username must be at least 3 characters';
}
return null;
},
// 保存回调(当调用save()时触发)
onSaved: (value) {
print('Username saved: $value');
},
),
const SizedBox(height: 16),
TextFormField(
controller: _emailController,
decoration: const InputDecoration(
labelText: 'Email',
border: OutlineInputBorder(),
),
keyboardType: TextInputType.emailAddress,
validator: (value) {
if (value == null || value.isEmpty) {
return 'Please enter email';
}
if (!RegExp(
r'^[\w-.]+@([\w-]+.)+[\w-]{2,4}$',
).hasMatch(value)) {
return 'Please enter a valid email';
}
return null;
},
),
const SizedBox(height: 24),
ElevatedButton(
onPressed: _submitForm,
child: const Text('Submit'),
),
],
),
),
),
);
}
}
表单处理流程:
- 用户输入数据
- 触发验证(手动或自动)
- 用户点击提交按钮
- 调用
_formKey.currentState!.validate()
验证所有字段 - 验证通过后,调用
_formKey.currentState!.save()
保存所有字段值 - 获取输入数据并进行处理(如提交到服务器)
3. 手动控制验证时机
除了自动验证,也可以手动控制验证时机:
dart
// 只在提交时验证
Form(
key: _formKey,
autovalidateMode: AutovalidateMode.disabled, // 禁用自动验证
// ...
)
// 手动触发单个字段验证
void _validateUsername() {
_usernameFieldKey.currentState?.validate();
}
// 重置表单
void _resetForm() {
_formKey.currentState?.reset();
}
三、文本控制器:TextEditingController
TextEditingController
用于控制文本输入组件的内容,能够获取、设置和监听输入内容的变化,是处理表单数据的重要工具。
1. 基本用法
dart
class TextEditingControllerExample extends StatefulWidget {
const TextEditingControllerExample({super.key});
@override
State<TextEditingControllerExample> createState() =>
_TextEditingControllerExampleState();
}
class _TextEditingControllerExampleState
extends State<TextEditingControllerExample> {
// 创建文本控制器
final _controller = TextEditingController();
@override
void initState() {
super.initState();
// 设置初始值
_controller.text = 'Initial value';
// 监听文本变化
_controller.addListener(_onTextChanged);
}
@override
void dispose() {
// 移除监听器并释放资源
_controller.removeListener(_onTextChanged);
_controller.dispose();
super.dispose();
}
// 文本变化回调
void _onTextChanged() {
print('Text changed: ${_controller.text}');
}
// 获取输入值
void _getValue() {
final text = _controller.text;
ScaffoldMessenger.of(
context,
).showSnackBar(SnackBar(content: Text('Current value: $text')));
}
// 设置输入值
void _setValue() {
_controller.text = 'New value set programmatically';
}
// 清空输入
void _clearValue() {
_controller.clear();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Text Controller')),
body: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
children: [
TextField(
controller: _controller, // 关联控制器
decoration: const InputDecoration(
labelText: 'Enter text',
border: OutlineInputBorder(),
),
),
const SizedBox(height: 16),
Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
ElevatedButton(
onPressed: _getValue,
child: const Text('Get Value'),
),
ElevatedButton(
onPressed: _setValue,
child: const Text('Set Value'),
),
ElevatedButton(
onPressed: _clearValue,
child: const Text('Clear'),
),
],
),
],
),
),
);
}
}
2. 高级用法
TextEditingController
还提供了一些高级功能:
- 选择文本:
dart
// 选择文本
_controller.selection = TextSelection(
baseOffset: 2,
extentOffset: 5,
);
// 选中所有文本
_controller.selection = TextSelection(
baseOffset: 0,
extentOffset: _controller.text.length,
);
- 设置光标位置:
dart
// 设置光标位置到末尾
_controller.selection = TextSelection.fromPosition(
TextPosition(offset: _controller.text.length),
);
- 带样式的文本:
使用 TextSpan
设置富文本:
dart
final _richTextController = TextEditingController(
text: 'Hello World',
);
// 在build方法中
TextField(
controller: _richTextController,
decoration: const InputDecoration(labelText: 'Rich Text'),
style: const TextStyle(fontSize: 16),
// 可以通过inputFormatters限制输入格式
)
四、实例:实现登录与注册表单
下面实现一个完整的登录和注册表单,包含输入验证、表单提交、密码显示 / 隐藏切换等功能。
1. 登录表单
dart
class LoginForm extends StatefulWidget {
const LoginForm({super.key});
@override
State<LoginForm> createState() => _LoginFormState();
}
class _LoginFormState extends State<LoginForm> {
final _formKey = GlobalKey<FormState>();
final _emailController = TextEditingController();
final _passwordController = TextEditingController();
bool _obscurePassword = true; // 控制密码是否显示
bool _isLoading = false; // 控制加载状态
@override
void dispose() {
_emailController.dispose();
_passwordController.dispose();
super.dispose();
}
// 切换密码显示状态
void _togglePasswordVisibility() {
setState(() {
_obscurePassword = !_obscurePassword;
});
}
// 提交登录表单
Future<void> _submitLogin() async {
if (_formKey.currentState!.validate()) {
setState(() {
_isLoading = true;
});
// 模拟登录请求
try {
await Future.delayed(const Duration(seconds: 2));
// 登录成功,导航到主页
if (mounted) {
ScaffoldMessenger.of(
context,
).showSnackBar(const SnackBar(content: Text('Login successful')));
// Navigator.pushReplacementNamed(context, '/home');
}
} catch (e) {
if (mounted) {
ScaffoldMessenger.of(
context,
).showSnackBar(SnackBar(content: Text('Login failed: $e')));
}
} finally {
if (mounted) {
setState(() {
_isLoading = false;
});
}
}
}
}
@override
Widget build(BuildContext context) {
return Form(
key: _formKey,
autovalidateMode: AutovalidateMode.onUserInteraction,
child: Column(
children: [
TextFormField(
controller: _emailController,
decoration: const InputDecoration(
labelText: 'Email',
prefixIcon: Icon(Icons.email),
border: OutlineInputBorder(),
),
keyboardType: TextInputType.emailAddress,
enabled: !_isLoading,
validator: (value) {
if (value == null || value.isEmpty) {
return 'Please enter your email';
}
if (!RegExp(
r'^[\w-.]+@([\w-]+.)+[\w-]{2,4}$',
).hasMatch(value)) {
return 'Please enter a valid email';
}
return null;
},
),
const SizedBox(height: 16),
TextFormField(
controller: _passwordController,
decoration: InputDecoration(
labelText: 'Password',
prefixIcon: const Icon(Icons.lock),
border: const OutlineInputBorder(),
// 密码可见性切换按钮
suffixIcon: IconButton(
icon: Icon(
_obscurePassword ? Icons.visibility : Icons.visibility_off,
),
onPressed: _togglePasswordVisibility,
),
),
obscureText: _obscurePassword,
enabled: !_isLoading,
validator: (value) {
if (value == null || value.isEmpty) {
return 'Please enter your password';
}
if (value.length < 6) {
return 'Password must be at least 6 characters';
}
return null;
},
),
const SizedBox(height: 8),
// 忘记密码链接
Align(
alignment: Alignment.centerRight,
child: TextButton(
onPressed: () {
// 导航到忘记密码页面
// Navigator.pushNamed(context, '/forgot-password');
},
child: const Text('Forgot Password?'),
),
),
const SizedBox(height: 16),
// 登录按钮
SizedBox(
width: double.infinity,
child: ElevatedButton(
onPressed: _isLoading ? null : _submitLogin,
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 16),
),
child: _isLoading
? const CircularProgressIndicator(color: Colors.white)
: const Text('Login'),
),
),
const SizedBox(height: 16),
// 注册链接
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Text("Don't have an account?"),
TextButton(
onPressed: () {
// 导航到注册页面
// Navigator.pushNamed(context, '/register');
},
child: const Text('Register'),
),
],
),
],
),
);
}
}
2. 注册表单
dart
class RegisterForm extends StatefulWidget {
const RegisterForm({super.key});
@override
State<RegisterForm> createState() => _RegisterFormState();
}
class _RegisterFormState extends State<RegisterForm> {
final _formKey = GlobalKey<FormState>();
final _usernameController = TextEditingController();
final _emailController = TextEditingController();
final _passwordController = TextEditingController();
final _confirmPasswordController = TextEditingController();
bool _obscurePassword = true;
bool _obscureConfirmPassword = true;
bool _isLoading = false;
String? _selectedGender;
@override
void dispose() {
_usernameController.dispose();
_emailController.dispose();
_passwordController.dispose();
_confirmPasswordController.dispose();
super.dispose();
}
void _togglePasswordVisibility() {
setState(() {
_obscurePassword = !_obscurePassword;
});
}
void _toggleConfirmPasswordVisibility() {
setState(() {
_obscureConfirmPassword = !_obscureConfirmPassword;
});
}
Future<void> _submitRegistration() async {
if (_formKey.currentState!.validate()) {
setState(() {
_isLoading = true;
});
// 模拟注册请求
try {
await Future.delayed(const Duration(seconds: 2));
// 注册成功
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Registration successful')),
);
Navigator.pop(context); // 返回登录页面
}
} catch (e) {
if (mounted) {
ScaffoldMessenger.of(
context,
).showSnackBar(SnackBar(content: Text('Registration failed: $e')));
}
} finally {
if (mounted) {
setState(() {
_isLoading = false;
});
}
}
}
}
@override
Widget build(BuildContext context) {
return Form(
key: _formKey,
autovalidateMode: AutovalidateMode.onUserInteraction,
child: SingleChildScrollView(
child: Column(
children: [
TextFormField(
controller: _usernameController,
decoration: const InputDecoration(
labelText: 'Username',
prefixIcon: Icon(Icons.person),
border: OutlineInputBorder(),
),
enabled: !_isLoading,
validator: (value) {
if (value == null || value.isEmpty) {
return 'Please enter your username';
}
if (value.length < 3) {
return 'Username must be at least 3 characters';
}
if (value.length > 20) {
return 'Username cannot exceed 20 characters';
}
return null;
},
),
const SizedBox(height: 16),
TextFormField(
controller: _emailController,
decoration: const InputDecoration(
labelText: 'Email',
prefixIcon: Icon(Icons.email),
border: OutlineInputBorder(),
),
keyboardType: TextInputType.emailAddress,
enabled: !_isLoading,
validator: (value) {
if (value == null || value.isEmpty) {
return 'Please enter your email';
}
if (!RegExp(
r'^[\w-.]+@([\w-]+.)+[\w-]{2,4}$',
).hasMatch(value)) {
return 'Please enter a valid email';
}
return null;
},
),
const SizedBox(height: 16),
// 性别选择
DropdownButtonFormField<String>(
value: _selectedGender,
decoration: const InputDecoration(
labelText: 'Gender',
prefixIcon: Icon(Icons.person_2),
border: OutlineInputBorder(),
),
items: const [
DropdownMenuItem(value: 'male', child: Text('Male')),
DropdownMenuItem(value: 'female', child: Text('Female')),
DropdownMenuItem(value: 'other', child: Text('Other')),
],
onChanged: (value) {
setState(() {
_selectedGender = value;
});
},
validator: (value) {
if (value == null) {
return 'Please select your gender';
}
return null;
},
),
const SizedBox(height: 16),
TextFormField(
controller: _passwordController,
decoration: InputDecoration(
labelText: 'Password',
prefixIcon: const Icon(Icons.lock),
border: const OutlineInputBorder(),
suffixIcon: IconButton(
icon: Icon(
_obscurePassword ? Icons.visibility : Icons.visibility_off,
),
onPressed: _togglePasswordVisibility,
),
),
obscureText: _obscurePassword,
enabled: !_isLoading,
validator: (value) {
if (value == null || value.isEmpty) {
return 'Please enter your password';
}
if (value.length < 6) {
return 'Password must be at least 6 characters';
}
if (!RegExp(r'[A-Z]').hasMatch(value)) {
return 'Password must contain at least one uppercase letter';
}
if (!RegExp(r'[0-9]').hasMatch(value)) {
return 'Password must contain at least one number';
}
return null;
},
),
const SizedBox(height: 16),
TextFormField(
controller: _confirmPasswordController,
decoration: InputDecoration(
labelText: 'Confirm Password',
prefixIcon: const Icon(Icons.lock_clock),
border: const OutlineInputBorder(),
suffixIcon: IconButton(
icon: Icon(
_obscureConfirmPassword
? Icons.visibility
: Icons.visibility_off,
),
onPressed: _toggleConfirmPasswordVisibility,
),
),
obscureText: _obscureConfirmPassword,
enabled: !_isLoading,
validator: (value) {
if (value == null || value.isEmpty) {
return 'Please confirm your password';
}
if (value != _passwordController.text) {
return 'Passwords do not match';
}
return null;
},
),
const SizedBox(height: 16),
// 同意条款
FormField<bool>(
initialValue: false,
validator: (value) {
if (value == false) {
return 'Please agree to the terms';
}
return null;
},
builder: (formFieldState) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Checkbox(
value: formFieldState.value,
onChanged: _isLoading
? null
: (value) {
formFieldState.didChange(value);
},
),
const Text(
'I agree to the Terms\n of Service and Privacy Policy',
),
],
),
if (formFieldState.hasError)
Padding(
padding: const EdgeInsets.only(top: 4, left: 4),
child: Text(
formFieldState.errorText!,
style: const TextStyle(
color: Colors.red,
fontSize: 12,
),
),
),
],
);
},
),
const SizedBox(height: 24),
// 注册按钮
SizedBox(
width: double.infinity,
child: ElevatedButton(
onPressed: _isLoading ? null : _submitRegistration,
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 16),
),
child: _isLoading
? const CircularProgressIndicator(color: Colors.white)
: const Text('Register'),
),
),
],
),
),
);
}
}
3. 表单页面集成
dart
class AuthScreen extends StatefulWidget {
const AuthScreen({super.key});
@override
State<AuthScreen> createState() => _AuthScreenState();
}
class _AuthScreenState extends State<AuthScreen> {
bool _isLogin = true; // 切换登录/注册模式
// 切换表单模式
void _toggleFormMode() {
setState(() {
_isLogin = !_isLogin;
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: Padding(
padding: const EdgeInsets.all(24.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// 标题
Text(
_isLogin ? 'Welcome Back' : 'Create Account',
style: const TextStyle(
fontSize: 28,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 8),
Text(
_isLogin
? 'Sign in to your account to continue'
: 'Fill in the details to create a new account',
style: const TextStyle(
color: Colors.grey,
fontSize: 16,
),
),
const SizedBox(height: 32),
// 显示登录或注册表单
_isLogin ? const LoginForm() : const RegisterForm(),
// 切换表单按钮
if (!_isLogin)
TextButton(
onPressed: _toggleFormMode,
child: const Text('Already have an account? Login'),
),
],
),
),
);
}
}