Flutter 表单组件深度解析

120 阅读11分钟

通过丰富的图表、对比分析和实际案例,全面掌握 Flutter 表单组件的使用技巧

Flutter Form Widgets Version License

📊 文章概览

章节内容难度等级
表单基础基础表单实现⭐⭐
输入组件各种输入控件⭐⭐⭐
选择组件选择器组件⭐⭐⭐
表单验证表单验证机制⭐⭐⭐⭐
表单状态管理状态管理策略⭐⭐⭐⭐
智能表单验证高级验证功能⭐⭐⭐⭐⭐
动态表单生成动态表单组件⭐⭐⭐⭐⭐

🎯 学习目标

  • ✅ 掌握表单组件的核心概念和使用方法
  • ✅ 学会表单验证和错误处理
  • ✅ 理解表单状态管理和性能优化
  • ✅ 能够实现复杂的表单应用
  • ✅ 掌握最佳实践和设计模式

📋 目录导航

🎯 快速导航

📋 概述

本文档详细介绍 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 表单组件提供了丰富的功能,通过本文的深入学习,你应该能够:

核心组件掌握

  1. TextFormField:文本输入的核心组件,支持丰富的自定义
  2. DropdownButtonFormField:下拉选择组件,适合选项较多的场景
  3. Checkbox/Radio:单选和多选组件,处理布尔值和单选逻辑
  4. Slider:滑块选择组件,适合数值范围选择
  5. AnimatedTextField:带动画效果的高级文本输入组件

高级功能实现

  1. 智能验证:使用组合验证器和实时验证提高用户体验
  2. 动态表单:根据配置动态生成表单组件
  3. 多步骤表单:处理复杂的分步骤数据收集
  4. 状态管理:合理管理表单状态和用户输入
  5. 动画效果:提供流畅的交互动画和视觉反馈

表单设计原则

  1. 简洁明了:避免不必要的字段,保持表单简洁
  2. 逻辑分组:相关字段分组显示,提升填写效率
  3. 即时反馈:提供实时验证和清晰的错误提示
  4. 渐进式披露:复杂表单采用多步骤或条件显示
  5. 无障碍支持:确保所有用户都能顺利使用表单

性能优化技巧

  1. 合理使用控制器:避免不必要的TextEditingController创建
  2. 延迟验证:在用户完成输入后再进行验证
  3. 状态管理:使用适当的状态管理方案处理复杂表单
  4. 内存管理:及时释放控制器和监听器资源

推荐工具和库

  • flutter_form_builder:强大的表单构建工具
  • reactive_forms:响应式表单框架
  • form_field_validator:丰富的表单验证工具
  • flutter_typeahead:自动完成输入组件
  • mask_text_input_formatter:输入格式化工具

下一步学习

记住,优秀的表单设计不仅仅是功能完整,更重要的是让用户感到简单、直观和愉悦。在实践中不断优化和创新,你一定能创建出用户喜爱的表单体验!