通过丰富的图表、对比分析和实际案例,全面掌握 Flutter 表单组件的使用技巧
📊 文章概览
| 章节 | 内容 | 难度等级 |
|---|---|---|
| 表单基础 | 基础表单实现 | ⭐⭐ |
| 输入组件 | 各种输入控件 | ⭐⭐⭐ |
| 选择组件 | 选择器组件 | ⭐⭐⭐ |
| 表单验证 | 表单验证机制 | ⭐⭐⭐⭐ |
| 表单状态管理 | 状态管理策略 | ⭐⭐⭐⭐ |
| 智能表单验证 | 高级验证功能 | ⭐⭐⭐⭐⭐ |
| 动态表单生成 | 动态表单组件 | ⭐⭐⭐⭐⭐ |
🎯 学习目标
- ✅ 掌握表单组件的核心概念和使用方法
- ✅ 学会表单验证和错误处理
- ✅ 理解表单状态管理和性能优化
- ✅ 能够实现复杂的表单应用
- ✅ 掌握最佳实践和设计模式
📋 目录导航
🎯 快速导航
📋 概述
本文档详细介绍 Flutter 中各种表单组件的使用方法、最佳实践和高级技巧。
🎯 表单基础
Form Widget
// lib/widgets/basic_form.dart
import 'package:flutter/material.dart';
class BasicFormExample extends StatefulWidget {
const BasicFormExample({super.key});
@override
State<BasicFormExample> createState() => _BasicFormExampleState();
}
class _BasicFormExampleState extends State<BasicFormExample> {
final _formKey = GlobalKey<FormState>();
final _nameController = TextEditingController();
final _emailController = TextEditingController();
final _passwordController = TextEditingController();
bool _isLoading = false;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('基础表单'),
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
),
body: Padding(
padding: const EdgeInsets.all(16.0),
child: Form(
key: _formKey,
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// 姓名输入
TextFormField(
controller: _nameController,
decoration: const InputDecoration(
labelText: '姓名',
hintText: '请输入您的姓名',
prefixIcon: Icon(Icons.person),
border: OutlineInputBorder(),
),
validator: (value) {
if (value == null || value.isEmpty) {
return '请输入姓名';
}
if (value.length < 2) {
return '姓名至少需要2个字符';
}
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.isEmpty) {
return '请输入邮箱';
}
if (!RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$').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.isEmpty) {
return '请输入密码';
}
if (value.length < 6) {
return '密码至少需要6个字符';
}
return null;
},
),
const SizedBox(height: 24),
// 提交按钮
ElevatedButton(
onPressed: _isLoading ? null : _submitForm,
child: _isLoading
? const SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(strokeWidth: 2),
)
: const Text('提交'),
),
],
),
),
),
);
}
Future<void> _submitForm() 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('表单提交成功!'),
backgroundColor: Colors.green,
),
);
// 清空表单
_formKey.currentState!.reset();
_nameController.clear();
_emailController.clear();
_passwordController.clear();
}
} catch (e) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('提交失败: $e'),
backgroundColor: Colors.red,
),
);
}
} finally {
if (mounted) {
setState(() {
_isLoading = false;
});
}
}
}
}
@override
void dispose() {
_nameController.dispose();
_emailController.dispose();
_passwordController.dispose();
super.dispose();
}
}
📝 输入组件
TextFormField 高级用法
// lib/widgets/advanced_text_field.dart
class AdvancedTextFieldExample extends StatefulWidget {
const AdvancedTextFieldExample({super.key});
@override
State<AdvancedTextFieldExample> createState() => _AdvancedTextFieldExampleState();
}
class _AdvancedTextFieldExampleState extends State<AdvancedTextFieldExample> {
final _phoneController = TextEditingController();
final _priceController = TextEditingController();
final _descriptionController = TextEditingController();
bool _obscurePassword = true;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('高级输入组件')),
body: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
children: [
// 手机号输入(带格式化)
TextFormField(
controller: _phoneController,
keyboardType: TextInputType.phone,
inputFormatters: [
FilteringTextInputFormatter.digitsOnly,
LengthLimitingTextInputFormatter(11),
PhoneNumberFormatter(),
],
decoration: const InputDecoration(
labelText: '手机号',
hintText: '138 0013 8000',
prefixIcon: Icon(Icons.phone),
border: OutlineInputBorder(),
),
validator: (value) {
if (value == null || value.isEmpty) {
return '请输入手机号';
}
final phone = value.replaceAll(' ', '');
if (!RegExp(r'^1[3-9]\d{9}$').hasMatch(phone)) {
return '请输入有效的手机号';
}
return null;
},
),
const SizedBox(height: 16),
// 价格输入(带货币格式)
TextFormField(
controller: _priceController,
keyboardType: const TextInputType.numberWithOptions(decimal: true),
inputFormatters: [
FilteringTextInputFormatter.allow(RegExp(r'^\d+\.?\d{0,2}')),
CurrencyFormatter(),
],
decoration: const InputDecoration(
labelText: '价格',
hintText: '¥0.00',
prefixIcon: Icon(Icons.attach_money),
border: OutlineInputBorder(),
),
),
const SizedBox(height: 16),
// 密码输入(带显示/隐藏切换)
TextFormField(
obscureText: _obscurePassword,
decoration: InputDecoration(
labelText: '密码',
hintText: '请输入密码',
prefixIcon: const Icon(Icons.lock),
suffixIcon: IconButton(
icon: Icon(
_obscurePassword ? Icons.visibility : Icons.visibility_off,
),
onPressed: () {
setState(() {
_obscurePassword = !_obscurePassword;
});
},
),
border: const OutlineInputBorder(),
),
),
const SizedBox(height: 16),
// 多行文本输入
TextFormField(
controller: _descriptionController,
maxLines: 4,
maxLength: 200,
decoration: const InputDecoration(
labelText: '描述',
hintText: '请输入详细描述...',
alignLabelWithHint: true,
border: OutlineInputBorder(),
),
buildCounter: (context, {required currentLength, required isFocused, maxLength}) {
return Text(
'$currentLength/${maxLength ?? 0}',
style: TextStyle(
color: currentLength > (maxLength ?? 0) * 0.8
? Colors.orange
: Colors.grey,
),
);
},
),
],
),
),
);
}
@override
void dispose() {
_phoneController.dispose();
_priceController.dispose();
_descriptionController.dispose();
super.dispose();
}
}
// 手机号格式化器
class PhoneNumberFormatter extends TextInputFormatter {
@override
TextEditingValue formatEditUpdate(
TextEditingValue oldValue,
TextEditingValue newValue,
) {
final text = newValue.text.replaceAll(' ', '');
final buffer = StringBuffer();
for (int i = 0; i < text.length; i++) {
if (i == 3 || i == 7) {
buffer.write(' ');
}
buffer.write(text[i]);
}
final formatted = buffer.toString();
return TextEditingValue(
text: formatted,
selection: TextSelection.collapsed(offset: formatted.length),
);
}
}
// 货币格式化器
class CurrencyFormatter extends TextInputFormatter {
@override
TextEditingValue formatEditUpdate(
TextEditingValue oldValue,
TextEditingValue newValue,
) {
if (newValue.text.isEmpty) {
return newValue;
}
final value = double.tryParse(newValue.text);
if (value == null) {
return oldValue;
}
final formatted = '¥${value.toStringAsFixed(2)}';
return TextEditingValue(
text: formatted,
selection: TextSelection.collapsed(offset: formatted.length),
);
}
}
🎛️ 选择组件
Dropdown 和 Checkbox 组件
// lib/widgets/selection_widgets.dart
class SelectionWidgetsExample extends StatefulWidget {
const SelectionWidgetsExample({super.key});
@override
State<SelectionWidgetsExample> createState() => _SelectionWidgetsExampleState();
}
class _SelectionWidgetsExampleState extends State<SelectionWidgetsExample> {
String? _selectedCity;
final List<String> _cities = ['北京', '上海', '广州', '深圳', '杭州'];
String? _selectedGender;
final List<String> _genders = ['男', '女', '其他'];
final Set<String> _selectedHobbies = {};
final List<String> _hobbies = ['阅读', '运动', '音乐', '旅行', '摄影', '编程'];
bool _agreeTerms = false;
bool _receiveNotifications = true;
double _ageRange = 25;
RangeValues _priceRange = const RangeValues(100, 500);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('选择组件')),
body: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 下拉选择
const Text('城市选择', style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold)),
const SizedBox(height: 8),
DropdownButtonFormField<String>(
value: _selectedCity,
decoration: const InputDecoration(
border: OutlineInputBorder(),
hintText: '请选择城市',
),
items: _cities.map((city) {
return DropdownMenuItem(
value: city,
child: Text(city),
);
}).toList(),
onChanged: (value) {
setState(() {
_selectedCity = value;
});
},
validator: (value) {
if (value == null) {
return '请选择城市';
}
return null;
},
),
const SizedBox(height: 24),
// 单选按钮组
const Text('性别', style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold)),
const SizedBox(height: 8),
Column(
children: _genders.map((gender) {
return RadioListTile<String>(
title: Text(gender),
value: gender,
groupValue: _selectedGender,
onChanged: (value) {
setState(() {
_selectedGender = value;
});
},
);
}).toList(),
),
const SizedBox(height: 24),
// 多选框组
const Text('兴趣爱好', style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold)),
const SizedBox(height: 8),
Wrap(
spacing: 8,
children: _hobbies.map((hobby) {
return FilterChip(
label: Text(hobby),
selected: _selectedHobbies.contains(hobby),
onSelected: (selected) {
setState(() {
if (selected) {
_selectedHobbies.add(hobby);
} else {
_selectedHobbies.remove(hobby);
}
});
},
);
}).toList(),
),
const SizedBox(height: 24),
// 开关和复选框
CheckboxListTile(
title: const Text('同意用户协议'),
value: _agreeTerms,
onChanged: (value) {
setState(() {
_agreeTerms = value ?? false;
});
},
controlAffinity: ListTileControlAffinity.leading,
),
SwitchListTile(
title: const Text('接收通知'),
subtitle: const Text('允许应用发送推送通知'),
value: _receiveNotifications,
onChanged: (value) {
setState(() {
_receiveNotifications = value;
});
},
),
const SizedBox(height: 24),
// 滑块
const Text('年龄', style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold)),
Slider(
value: _ageRange,
min: 18,
max: 60,
divisions: 42,
label: '${_ageRange.round()}岁',
onChanged: (value) {
setState(() {
_ageRange = value;
});
},
),
const SizedBox(height: 16),
// 范围滑块
const Text('价格区间', style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold)),
RangeSlider(
values: _priceRange,
min: 0,
max: 1000,
divisions: 20,
labels: RangeLabels(
'¥${_priceRange.start.round()}',
'¥${_priceRange.end.round()}',
),
onChanged: (values) {
setState(() {
_priceRange = values;
});
},
),
const Spacer(),
// 提交按钮
SizedBox(
width: double.infinity,
child: ElevatedButton(
onPressed: _agreeTerms ? _submitSelection : null,
child: const Text('提交选择'),
),
),
],
),
),
);
}
void _submitSelection() {
final result = {
'城市': _selectedCity,
'性别': _selectedGender,
'兴趣爱好': _selectedHobbies.toList(),
'年龄': _ageRange.round(),
'价格区间': '¥${_priceRange.start.round()} - ¥${_priceRange.end.round()}',
'接收通知': _receiveNotifications,
};
showDialog(
context: context,
builder: (context) {
return AlertDialog(
title: const Text('选择结果'),
content: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: result.entries.map((entry) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 4),
child: Text('${entry.key}: ${entry.value}'),
);
}).toList(),
),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: const Text('确定'),
),
],
);
},
);
}
}
✅ 表单验证
自定义验证器
// lib/utils/form_validators.dart
class FormValidators {
// 必填验证
static String? required(String? value, [String? fieldName]) {
if (value == null || value.trim().isEmpty) {
return '${fieldName ?? '此字段'}不能为空';
}
return null;
}
// 邮箱验证
static String? email(String? value) {
if (value == null || value.isEmpty) {
return null; // 允许为空,如需必填请配合 required 使用
}
final emailRegex = RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$');
if (!emailRegex.hasMatch(value)) {
return '请输入有效的邮箱地址';
}
return null;
}
// 手机号验证
static String? phone(String? value) {
if (value == null || value.isEmpty) {
return null;
}
final phone = value.replaceAll(RegExp(r'\s+'), '');
final phoneRegex = RegExp(r'^1[3-9]\d{9}$');
if (!phoneRegex.hasMatch(phone)) {
return '请输入有效的手机号';
}
return null;
}
// 密码强度验证
static String? password(String? value) {
if (value == null || value.isEmpty) {
return null;
}
if (value.length < 8) {
return '密码至少需要8个字符';
}
if (!RegExp(r'^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)').hasMatch(value)) {
return '密码必须包含大小写字母和数字';
}
return null;
}
// 长度验证
static String? length(String? value, int min, [int? max]) {
if (value == null || value.isEmpty) {
return null;
}
if (value.length < min) {
return '至少需要$min个字符';
}
if (max != null && value.length > max) {
return '最多只能输入$max个字符';
}
return null;
}
// 数字范围验证
static String? numberRange(String? value, double min, double max) {
if (value == null || value.isEmpty) {
return null;
}
final number = double.tryParse(value);
if (number == null) {
return '请输入有效的数字';
}
if (number < min || number > max) {
return '请输入$min到$max之间的数字';
}
return null;
}
// 确认密码验证
static String? confirmPassword(String? value, String? originalPassword) {
if (value == null || value.isEmpty) {
return null;
}
if (value != originalPassword) {
return '两次输入的密码不一致';
}
return null;
}
// 组合验证器
static String? combine(String? value, List<String? Function(String?)> validators) {
for (final validator in validators) {
final result = validator(value);
if (result != null) {
return result;
}
}
return null;
}
}
// 使用示例
class ValidatedFormExample extends StatefulWidget {
const ValidatedFormExample({super.key});
@override
State<ValidatedFormExample> createState() => _ValidatedFormExampleState();
}
class _ValidatedFormExampleState extends State<ValidatedFormExample> {
final _formKey = GlobalKey<FormState>();
final _emailController = TextEditingController();
final _passwordController = TextEditingController();
final _confirmPasswordController = TextEditingController();
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('表单验证示例')),
body: Padding(
padding: const EdgeInsets.all(16.0),
child: Form(
key: _formKey,
child: Column(
children: [
TextFormField(
controller: _emailController,
decoration: const InputDecoration(
labelText: '邮箱',
border: OutlineInputBorder(),
),
validator: (value) => FormValidators.combine(value, [
(v) => FormValidators.required(v, '邮箱'),
FormValidators.email,
]),
),
const SizedBox(height: 16),
TextFormField(
controller: _passwordController,
obscureText: true,
decoration: const InputDecoration(
labelText: '密码',
border: OutlineInputBorder(),
),
validator: (value) => FormValidators.combine(value, [
(v) => FormValidators.required(v, '密码'),
FormValidators.password,
]),
),
const SizedBox(height: 16),
TextFormField(
controller: _confirmPasswordController,
obscureText: true,
decoration: const InputDecoration(
labelText: '确认密码',
border: OutlineInputBorder(),
),
validator: (value) => FormValidators.combine(value, [
(v) => FormValidators.required(v, '确认密码'),
(v) => FormValidators.confirmPassword(v, _passwordController.text),
]),
),
const SizedBox(height: 24),
ElevatedButton(
onPressed: () {
if (_formKey.currentState!.validate()) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('验证通过!')),
);
}
},
child: const Text('提交'),
),
],
),
),
),
);
}
@override
void dispose() {
_emailController.dispose();
_passwordController.dispose();
_confirmPasswordController.dispose();
super.dispose();
}
}
🎨 自定义表单组件
自定义输入组件
// lib/widgets/custom_form_field.dart
class CustomFormField extends StatelessWidget {
const CustomFormField({
super.key,
required this.label,
this.hintText,
this.controller,
this.validator,
this.keyboardType,
this.obscureText = false,
this.maxLines = 1,
this.prefixIcon,
this.suffixIcon,
this.enabled = true,
this.onChanged,
this.inputFormatters,
});
final String label;
final String? hintText;
final TextEditingController? controller;
final String? Function(String?)? validator;
final TextInputType? keyboardType;
final bool obscureText;
final int maxLines;
final Widget? prefixIcon;
final Widget? suffixIcon;
final bool enabled;
final void Function(String)? onChanged;
final List<TextInputFormatter>? inputFormatters;
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
label,
style: Theme.of(context).textTheme.titleSmall?.copyWith(
fontWeight: FontWeight.w600,
),
),
const SizedBox(height: 8),
TextFormField(
controller: controller,
validator: validator,
keyboardType: keyboardType,
obscureText: obscureText,
maxLines: maxLines,
enabled: enabled,
onChanged: onChanged,
inputFormatters: inputFormatters,
decoration: InputDecoration(
hintText: hintText,
prefixIcon: prefixIcon,
suffixIcon: suffixIcon,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(
color: Colors.grey.shade300,
),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(
color: Theme.of(context).primaryColor,
width: 2,
),
),
errorBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: const BorderSide(
color: Colors.red,
),
),
filled: true,
fillColor: enabled ? Colors.grey.shade50 : Colors.grey.shade100,
),
),
],
);
}
}
// 自定义选择器组件
class CustomSelector<T> extends StatelessWidget {
const CustomSelector({
super.key,
required this.label,
required this.value,
required this.items,
required this.onChanged,
this.hintText,
this.validator,
});
final String label;
final T? value;
final List<DropdownMenuItem<T>> items;
final void Function(T?) onChanged;
final String? hintText;
final String? Function(T?)? validator;
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
label,
style: Theme.of(context).textTheme.titleSmall?.copyWith(
fontWeight: FontWeight.w600,
),
),
const SizedBox(height: 8),
DropdownButtonFormField<T>(
value: value,
items: items,
onChanged: onChanged,
validator: validator,
decoration: InputDecoration(
hintText: hintText,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(
color: Colors.grey.shade300,
),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(
color: Theme.of(context).primaryColor,
width: 2,
),
),
filled: true,
fillColor: Colors.grey.shade50,
),
),
],
);
}
}
🧠 智能表单验证
高级验证器组合
// lib/utils/smart_validators.dart
class SmartFormValidator {
static 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;
}
static String? validatePassword(String? value) {
if (value == null || value.isEmpty) {
return '请输入密码';
}
if (value.length < 8) {
return '密码长度至少8位';
}
if (!RegExp(r'^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)').hasMatch(value)) {
return '密码必须包含大小写字母和数字';
}
return null;
}
static 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;
}
static String? validateRequired(String? value, String fieldName) {
if (value == null || value.trim().isEmpty) {
return '请输入$fieldName';
}
return null;
}
static String? validateLength(String? value, int minLength, int maxLength, String fieldName) {
if (value == null || value.isEmpty) {
return '请输入$fieldName';
}
if (value.length < minLength) {
return '$fieldName长度不能少于$minLength位';
}
if (value.length > maxLength) {
return '$fieldName长度不能超过$maxLength位';
}
return null;
}
}
// 带动画的智能输入框
class AnimatedTextField extends StatefulWidget {
final String label;
final String? hint;
final IconData? prefixIcon;
final bool obscureText;
final TextInputType keyboardType;
final String? Function(String?)? validator;
final Function(String)? onChanged;
final TextEditingController? controller;
final bool enabled;
const AnimatedTextField({
Key? key,
required this.label,
this.hint,
this.prefixIcon,
this.obscureText = false,
this.keyboardType = TextInputType.text,
this.validator,
this.onChanged,
this.controller,
this.enabled = true,
}) : super(key: key);
@override
_AnimatedTextFieldState createState() => _AnimatedTextFieldState();
}
class _AnimatedTextFieldState extends State<AnimatedTextField>
with SingleTickerProviderStateMixin {
late AnimationController _animationController;
late Animation<double> _labelAnimation;
late Animation<Color?> _borderColorAnimation;
final FocusNode _focusNode = FocusNode();
bool _hasError = false;
bool _isFocused = false;
bool _hasText = false;
@override
void initState() {
super.initState();
_animationController = AnimationController(
duration: Duration(milliseconds: 200),
vsync: this,
);
_labelAnimation = Tween<double>(
begin: 0.0,
end: 1.0,
).animate(CurvedAnimation(
parent: _animationController,
curve: Curves.easeInOut,
));
_borderColorAnimation = ColorTween(
begin: Colors.grey[400],
end: Colors.blue,
).animate(_animationController);
_focusNode.addListener(_onFocusChange);
if (widget.controller != null) {
widget.controller!.addListener(_onTextChange);
_hasText = widget.controller!.text.isNotEmpty;
}
}
@override
void dispose() {
_animationController.dispose();
_focusNode.dispose();
super.dispose();
}
void _onFocusChange() {
setState(() {
_isFocused = _focusNode.hasFocus;
});
if (_isFocused || _hasText) {
_animationController.forward();
} else {
_animationController.reverse();
}
}
void _onTextChange() {
final hasText = widget.controller!.text.isNotEmpty;
if (hasText != _hasText) {
setState(() {
_hasText = hasText;
});
if (_hasText || _isFocused) {
_animationController.forward();
} else {
_animationController.reverse();
}
}
}
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: _animationController,
builder: (context, child) {
return Container(
margin: EdgeInsets.symmetric(vertical: 8),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
decoration: BoxDecoration(
border: Border.all(
color: _hasError
? Colors.red
: (_borderColorAnimation.value ?? Colors.grey[400]!),
width: _isFocused ? 2.0 : 1.0,
),
borderRadius: BorderRadius.circular(8),
),
child: Stack(
children: [
TextFormField(
controller: widget.controller,
focusNode: _focusNode,
obscureText: widget.obscureText,
keyboardType: widget.keyboardType,
enabled: widget.enabled,
onChanged: widget.onChanged,
validator: (value) {
final error = widget.validator?.call(value);
setState(() {
_hasError = error != null;
});
return error;
},
decoration: InputDecoration(
hintText: widget.hint,
prefixIcon: widget.prefixIcon != null
? Icon(
widget.prefixIcon,
color: _isFocused ? Colors.blue : Colors.grey[600],
)
: null,
border: InputBorder.none,
contentPadding: EdgeInsets.symmetric(
horizontal: 16,
vertical: 16,
),
),
),
// 浮动标签
Positioned(
left: widget.prefixIcon != null ? 48 : 16,
top: _labelAnimation.value * 8 + 8,
child: Transform.scale(
scale: 1.0 - (_labelAnimation.value * 0.2),
alignment: Alignment.centerLeft,
child: Text(
widget.label,
style: TextStyle(
color: _hasError
? Colors.red
: (_isFocused ? Colors.blue : Colors.grey[600]),
fontSize: 16 - (_labelAnimation.value * 2),
fontWeight: _isFocused ? FontWeight.w500 : FontWeight.normal,
),
),
),
),
],
),
),
],
),
);
},
);
}
}
🔄 动态表单生成
多步骤表单组件
// lib/widgets/multi_step_form.dart
class MultiStepForm extends StatefulWidget {
final List<FormStep> steps;
final Function(Map<String, dynamic>) onCompleted;
final String? title;
const MultiStepForm({
Key? key,
required this.steps,
required this.onCompleted,
this.title,
}) : super(key: key);
@override
_MultiStepFormState createState() => _MultiStepFormState();
}
class _MultiStepFormState extends State<MultiStepForm> {
int _currentStep = 0;
final Map<String, dynamic> _formData = {};
final List<GlobalKey<FormState>> _formKeys = [];
@override
void initState() {
super.initState();
for (int i = 0; i < widget.steps.length; i++) {
_formKeys.add(GlobalKey<FormState>());
}
}
bool _validateCurrentStep() {
return _formKeys[_currentStep].currentState?.validate() ?? false;
}
void _nextStep() {
if (_validateCurrentStep()) {
// 保存当前步骤的数据
_formKeys[_currentStep].currentState?.save();
if (_currentStep < widget.steps.length - 1) {
setState(() {
_currentStep++;
});
} else {
// 完成表单
widget.onCompleted(_formData);
}
}
}
void _previousStep() {
if (_currentStep > 0) {
setState(() {
_currentStep--;
});
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title ?? '多步骤表单'),
backgroundColor: Colors.transparent,
elevation: 0,
),
body: Column(
children: [
// 步骤指示器
Container(
padding: EdgeInsets.all(16),
child: Row(
children: List.generate(widget.steps.length, (index) {
return Expanded(
child: Row(
children: [
Expanded(
child: Container(
height: 4,
decoration: BoxDecoration(
color: index <= _currentStep
? Colors.blue
: Colors.grey[300],
borderRadius: BorderRadius.circular(2),
),
),
),
if (index < widget.steps.length - 1)
SizedBox(width: 8),
],
),
);
}),
),
),
// 步骤标题
Padding(
padding: EdgeInsets.symmetric(horizontal: 16),
child: Row(
children: [
CircleAvatar(
radius: 16,
backgroundColor: Colors.blue,
child: Text(
'${_currentStep + 1}',
style: TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
),
),
),
SizedBox(width: 12),
Expanded(
child: Text(
widget.steps[_currentStep].title,
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
),
],
),
),
// 表单内容
Expanded(
child: Padding(
padding: EdgeInsets.all(16),
child: Form(
key: _formKeys[_currentStep],
child: widget.steps[_currentStep].content,
),
),
),
// 导航按钮
Container(
padding: EdgeInsets.all(16),
child: Row(
children: [
if (_currentStep > 0)
Expanded(
child: OutlinedButton(
onPressed: _previousStep,
child: Text('上一步'),
),
),
if (_currentStep > 0) SizedBox(width: 16),
Expanded(
child: ElevatedButton(
onPressed: _nextStep,
child: Text(
_currentStep == widget.steps.length - 1 ? '完成' : '下一步',
),
),
),
],
),
),
],
),
);
}
}
class FormStep {
final String title;
final Widget content;
final Map<String, dynamic>? data;
FormStep({
required this.title,
required this.content,
this.data,
});
}
// 动态表单生成器
class DynamicFormGenerator extends StatefulWidget {
final List<FormFieldConfig> fieldConfigs;
final Function(Map<String, dynamic>) onSubmit;
final String submitButtonText;
const DynamicFormGenerator({
Key? key,
required this.fieldConfigs,
required this.onSubmit,
this.submitButtonText = '提交',
}) : super(key: key);
@override
_DynamicFormGeneratorState createState() => _DynamicFormGeneratorState();
}
class _DynamicFormGeneratorState extends State<DynamicFormGenerator> {
final GlobalKey<FormState> _formKey = GlobalKey<FormState>();
final Map<String, dynamic> _formData = {};
final Map<String, TextEditingController> _controllers = {};
@override
void initState() {
super.initState();
for (final config in widget.fieldConfigs) {
if (config.type == FormFieldType.text ||
config.type == FormFieldType.email ||
config.type == FormFieldType.password) {
_controllers[config.key] = TextEditingController();
}
}
}
@override
void dispose() {
_controllers.values.forEach((controller) => controller.dispose());
super.dispose();
}
Widget _buildField(FormFieldConfig config) {
switch (config.type) {
case FormFieldType.text:
case FormFieldType.email:
case FormFieldType.password:
return AnimatedTextField(
label: config.label,
hint: config.hint,
prefixIcon: config.icon,
obscureText: config.type == FormFieldType.password,
keyboardType: config.type == FormFieldType.email
? TextInputType.emailAddress
: TextInputType.text,
controller: _controllers[config.key],
validator: config.validator,
onChanged: (value) {
_formData[config.key] = value;
},
);
case FormFieldType.dropdown:
return DropdownButtonFormField<String>(
decoration: InputDecoration(
labelText: config.label,
border: OutlineInputBorder(),
),
items: config.options?.map((option) {
return DropdownMenuItem<String>(
value: option,
child: Text(option),
);
}).toList(),
onChanged: (value) {
setState(() {
_formData[config.key] = value;
});
},
validator: config.validator,
);
case FormFieldType.checkbox:
return CheckboxListTile(
title: Text(config.label),
value: _formData[config.key] ?? false,
onChanged: (value) {
setState(() {
_formData[config.key] = value;
});
},
);
case FormFieldType.radio:
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
config.label,
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w500,
),
),
...config.options!.map((option) {
return RadioListTile<String>(
title: Text(option),
value: option,
groupValue: _formData[config.key],
onChanged: (value) {
setState(() {
_formData[config.key] = value;
});
},
);
}).toList(),
],
);
default:
return SizedBox.shrink();
}
}
void _submitForm() {
if (_formKey.currentState?.validate() ?? false) {
_formKey.currentState?.save();
widget.onSubmit(_formData);
}
}
@override
Widget build(BuildContext context) {
return Form(
key: _formKey,
child: Column(
children: [
...widget.fieldConfigs.map((config) {
return Padding(
padding: EdgeInsets.symmetric(vertical: 8),
child: _buildField(config),
);
}).toList(),
SizedBox(height: 24),
SizedBox(
width: double.infinity,
child: ElevatedButton(
onPressed: _submitForm,
style: ElevatedButton.styleFrom(
padding: EdgeInsets.symmetric(vertical: 16),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
),
child: Text(
widget.submitButtonText,
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
),
),
],
),
);
}
}
enum FormFieldType {
text,
email,
password,
dropdown,
checkbox,
radio,
}
class FormFieldConfig {
final String key;
final String label;
final FormFieldType type;
final String? hint;
final IconData? icon;
final List<String>? options;
final String? Function(String?)? validator;
final bool required;
FormFieldConfig({
required this.key,
required this.label,
required this.type,
this.hint,
this.icon,
this.options,
this.validator,
this.required = false,
});
}
📊 总结
Flutter 表单组件提供了丰富的功能,通过本文的深入学习,你应该能够:
核心组件掌握
- TextFormField:文本输入的核心组件,支持丰富的自定义
- DropdownButtonFormField:下拉选择组件,适合选项较多的场景
- Checkbox/Radio:单选和多选组件,处理布尔值和单选逻辑
- Slider:滑块选择组件,适合数值范围选择
- AnimatedTextField:带动画效果的高级文本输入组件
高级功能实现
- 智能验证:使用组合验证器和实时验证提高用户体验
- 动态表单:根据配置动态生成表单组件
- 多步骤表单:处理复杂的分步骤数据收集
- 状态管理:合理管理表单状态和用户输入
- 动画效果:提供流畅的交互动画和视觉反馈
表单设计原则
- 简洁明了:避免不必要的字段,保持表单简洁
- 逻辑分组:相关字段分组显示,提升填写效率
- 即时反馈:提供实时验证和清晰的错误提示
- 渐进式披露:复杂表单采用多步骤或条件显示
- 无障碍支持:确保所有用户都能顺利使用表单
性能优化技巧
- 合理使用控制器:避免不必要的TextEditingController创建
- 延迟验证:在用户完成输入后再进行验证
- 状态管理:使用适当的状态管理方案处理复杂表单
- 内存管理:及时释放控制器和监听器资源
推荐工具和库
- flutter_form_builder:强大的表单构建工具
- reactive_forms:响应式表单框架
- form_field_validator:丰富的表单验证工具
- flutter_typeahead:自动完成输入组件
- mask_text_input_formatter:输入格式化工具
下一步学习
记住,优秀的表单设计不仅仅是功能完整,更重要的是让用户感到简单、直观和愉悦。在实践中不断优化和创新,你一定能创建出用户喜爱的表单体验!