Flutter 表单处理完全指南

引言

表单是移动应用中不可或缺的一部分,Flutter 提供了强大的表单处理能力。本文将深入探讨 Flutter 表单的各种用法和高级技巧。

基础概念回顾

核心组件

  • Form: 表单容器
  • TextFormField: 文本输入字段
  • FormState: 表单状态管理
  • GlobalKey: 全局键用于访问表单状态

基本语法

final _formKey = GlobalKey<FormState>();

Form(
  key: _formKey,
  child: Column(
    children: [
      TextFormField(
        validator: (value) {
          if (value == null || value.isEmpty) {
            return 'Please enter some text';
          }
          return null;
        },
      ),
      ElevatedButton(
        onPressed: () {
          if (_formKey.currentState!.validate()) {
            // 表单验证通过
          }
        },
        child: const Text('Submit'),
      ),
    ],
  ),
)

高级技巧一:表单验证

基础验证

TextFormField(
  decoration: const InputDecoration(labelText: 'Email'),
  validator: (value) {
    if (value == null || value.isEmpty) {
      return 'Please enter email';
    }
    if (!RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$').hasMatch(value)) {
      return 'Please enter valid email';
    }
    return null;
  },
)

自定义验证器

String? validatePassword(String? value) {
  if (value == null || value.isEmpty) {
    return 'Please enter password';
  }
  if (value.length < 8) {
    return 'Password must be at least 8 characters';
  }
  if (!RegExp(r'[A-Z]').hasMatch(value)) {
    return 'Password must contain uppercase letter';
  }
  return null;
}

TextFormField(
  obscureText: true,
  decoration: const InputDecoration(labelText: 'Password'),
  validator: validatePassword,
)

验证多个字段

Form(
  key: _formKey,
  child: Column(
    children: [
      TextFormField(
        decoration: const InputDecoration(labelText: 'Password'),
        obscureText: true,
        controller: _passwordController,
        validator: (value) {
          if (value == null || value.isEmpty) {
            return 'Please enter password';
          }
          return null;
        },
      ),
      TextFormField(
        decoration: const InputDecoration(labelText: 'Confirm Password'),
        obscureText: true,
        validator: (value) {
          if (value != _passwordController.text) {
            return 'Passwords do not match';
          }
          return null;
        },
      ),
    ],
  ),
)

高级技巧二:表单状态管理

使用 TextEditingController

final _emailController = TextEditingController();
final _passwordController = TextEditingController();

@override
void dispose() {
  _emailController.dispose();
  _passwordController.dispose();
  super.dispose();
}

TextFormField(
  controller: _emailController,
  decoration: const InputDecoration(labelText: 'Email'),
)

监听文本变化

@override
void initState() {
  super.initState();
  _emailController.addListener(() {
    setState(() {
      _isEmailValid = _emailController.text.isNotEmpty;
    });
  });
}

重置表单

ElevatedButton(
  onPressed: () {
    _formKey.currentState!.reset();
    _emailController.clear();
    _passwordController.clear();
  },
  child: const Text('Reset'),
)

高级技巧三:自定义表单字段

创建自定义字段

class CustomTextField extends StatelessWidget {
  final String label;
  final TextEditingController? controller;
  final String? Function(String?)? validator;

  const CustomTextField({
    super.key,
    required this.label,
    this.controller,
    this.validator,
  });

  @override
  Widget build(BuildContext context) {
    return TextFormField(
      controller: controller,
      decoration: InputDecoration(
        labelText: label,
        border: OutlineInputBorder(
          borderRadius: BorderRadius.circular(8),
        ),
        focusedBorder: OutlineInputBorder(
          borderRadius: BorderRadius.circular(8),
          borderSide: const BorderSide(color: Colors.blue),
        ),
      ),
      validator: validator,
    );
  }
}

日期选择器字段

class DatePickerField extends StatefulWidget {
  final String label;
  final DateTime? initialDate;

  const DatePickerField({
    super.key,
    required this.label,
    this.initialDate,
  });

  @override
  State<DatePickerField> createState() => _DatePickerFieldState();
}

class _DatePickerFieldState extends State<DatePickerField> {
  DateTime? _selectedDate;

  Future<void> _selectDate() async {
    final picked = await showDatePicker(
      context: context,
      initialDate: _selectedDate ?? widget.initialDate ?? DateTime.now(),
      firstDate: DateTime(2000),
      lastDate: DateTime(2100),
    );
    if (picked != null) {
      setState(() {
        _selectedDate = picked;
      });
    }
  }

  @override
  Widget build(BuildContext context) {
    return TextFormField(
      readOnly: true,
      onTap: _selectDate,
      decoration: InputDecoration(
        labelText: widget.label,
        hintText: _selectedDate?.toLocal().toString().split(' ')[0],
      ),
      validator: (value) {
        if (_selectedDate == null) {
          return 'Please select a date';
        }
        return null;
      },
    );
  }
}

实战案例:登录表单

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 _isLoading = false;

  Future<void> _submit() async {
    if (_formKey.currentState!.validate()) {
      setState(() => _isLoading = true);
      
      try {
        await authService.login(
          _emailController.text,
          _passwordController.text,
        );
        // 导航到主页
      } catch (e) {
        ScaffoldMessenger.of(context).showSnackBar(
          SnackBar(content: Text('Error: $e')),
        );
      } finally {
        setState(() => _isLoading = false);
      }
    }
  }

  @override
  void dispose() {
    _emailController.dispose();
    _passwordController.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Form(
      key: _formKey,
      child: Column(
        children: [
          CustomTextField(
            label: 'Email',
            controller: _emailController,
            validator: (value) {
              if (value == null || value.isEmpty) {
                return 'Please enter email';
              }
              if (!RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$').hasMatch(value)) {
                return 'Please enter valid email';
              }
              return null;
            },
          ),
          const SizedBox(height: 16),
          CustomTextField(
            label: 'Password',
            controller: _passwordController,
            validator: (value) {
              if (value == null || value.isEmpty) {
                return 'Please enter password';
              }
              if (value.length < 8) {
                return 'Password must be at least 8 characters';
              }
              return null;
            },
          ),
          const SizedBox(height: 24),
          ElevatedButton(
            onPressed: _isLoading ? null : _submit,
            child: _isLoading 
                ? const CircularProgressIndicator()
                : const Text('Login'),
          ),
        ],
      ),
    );
  }
}

实战案例:注册表单

class RegistrationForm extends StatefulWidget {
  const RegistrationForm({super.key});

  @override
  State<RegistrationForm> createState() => _RegistrationFormState();
}

class _RegistrationFormState extends State<RegistrationForm> {
  final _formKey = GlobalKey<FormState>();
  final _emailController = TextEditingController();
  final _passwordController = TextEditingController();
  final _confirmPasswordController = TextEditingController();
  String? _selectedRole;

  @override
  void dispose() {
    _emailController.dispose();
    _passwordController.dispose();
    _confirmPasswordController.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Form(
      key: _formKey,
      child: Column(
        children: [
          CustomTextField(
            label: 'Email',
            controller: _emailController,
            validator: (value) {
              if (value == null || value.isEmpty) return 'Please enter email';
              if (!RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$').hasMatch(value)) {
                return 'Please enter valid email';
              }
              return null;
            },
          ),
          const SizedBox(height: 16),
          CustomTextField(
            label: 'Password',
            controller: _passwordController,
            validator: (value) {
              if (value == null || value.isEmpty) return 'Please enter password';
              if (value.length < 8) return 'Password must be at least 8 characters';
              return null;
            },
          ),
          const SizedBox(height: 16),
          CustomTextField(
            label: 'Confirm Password',
            controller: _confirmPasswordController,
            validator: (value) {
              if (value != _passwordController.text) {
                return 'Passwords do not match';
              }
              return null;
            },
          ),
          const SizedBox(height: 16),
          DropdownButtonFormField<String>(
            value: _selectedRole,
            hint: const Text('Select role'),
            items: const [
              DropdownMenuItem(value: 'user', child: Text('User')),
              DropdownMenuItem(value: 'admin', child: Text('Admin')),
            ],
            onChanged: (value) => setState(() => _selectedRole = value),
            validator: (value) => value == null ? 'Please select role' : null,
          ),
          const SizedBox(height: 24),
          ElevatedButton(
            onPressed: () {
              if (_formKey.currentState!.validate()) {
                // 提交表单
              }
            },
            child: const Text('Register'),
          ),
        ],
      ),
    );
  }
}

实战案例:表单验证动画

class AnimatedFormField extends StatefulWidget {
  final String label;
  final TextEditingController? controller;
  final String? Function(String?)? validator;

  const AnimatedFormField({
    super.key,
    required this.label,
    this.controller,
    this.validator,
  });

  @override
  State<AnimatedFormField> createState() => _AnimatedFormFieldState();
}

class _AnimatedFormFieldState extends State<AnimatedFormField> {
  bool _isFocused = false;
  bool _isValid = true;

  @override
  Widget build(BuildContext context) {
    return FocusScope(
      child: Focus(
        onFocusChange: (focused) {
          setState(() => _isFocused = focused);
          if (!focused) {
            setState(() => _isValid = widget.validator?.call(widget.controller?.text) == null);
          }
        },
        child: AnimatedContainer(
          duration: const Duration(milliseconds: 200),
          padding: _isFocused ? const EdgeInsets.all(4) : const EdgeInsets.all(0),
          decoration: BoxDecoration(
            borderRadius: BorderRadius.circular(8),
            border: Border.all(
              color: _isFocused 
                  ? (_isValid ? Colors.blue : Colors.red)
                  : Colors.grey,
              width: _isFocused ? 2 : 1,
            ),
          ),
          child: TextFormField(
            controller: widget.controller,
            decoration: InputDecoration(
              labelText: widget.label,
              border: InputBorder.none,
              contentPadding: const EdgeInsets.all(12),
            ),
            validator: widget.validator,
          ),
        ),
      ),
    );
  }
}

常见问题与解决方案

Q1:表单验证不生效?

A:确保使用 Form 包裹并提供 GlobalKey:

final _formKey = GlobalKey<FormState>();

Form(
  key: _formKey,
  child: TextFormField(validator: (value) {...}),
)

Q2:TextEditingController 内存泄漏?

A:在 dispose 中释放:

@override
void dispose() {
  _controller.dispose();
  super.dispose();
}

Q3:如何获取表单值?

A:使用 TextEditingController:

final value = _controller.text;

最佳实践

1. 分离验证逻辑

// validators.dart
String? validateEmail(String? value) {
  if (value == null || value.isEmpty) return 'Please enter email';
  if (!RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$').hasMatch(value)) {
    return 'Please enter valid email';
  }
  return null;
}

2. 使用 AutovalidateMode

Form(
  autovalidateMode: AutovalidateMode.onUserInteraction,
  child: TextFormField(validator: validateEmail),
)

3. 处理加载状态

ElevatedButton(
  onPressed: _isLoading ? null : _submit,
  child: _isLoading 
      ? const CircularProgressIndicator()
      : const Text('Submit'),
)

总结

Flutter 的表单处理功能非常强大。通过本文的学习,你应该能够:

  1. 创建和验证表单
  2. 使用 TextEditingController
  3. 创建自定义表单字段
  4. 实现表单验证动画
  5. 处理表单提交和加载状态

掌握这些技巧,能够帮助你创建更加用户友好的表单体验。

更多推荐