Flutter表单处理与验证完全指南

引言

表单是移动应用中最常见的交互元素之一,用于收集用户输入并进行验证。Flutter提供了强大的表单处理能力,从基础的TextField到复杂的表单验证,都有很好的支持。本文将深入探讨Flutter表单处理的各种技术和最佳实践。

基础表单组件

TextField组件

TextField(
  decoration: InputDecoration(
    labelText: '用户名',
    hintText: '请输入用户名',
    prefixIcon: const Icon(Icons.person),
    border: const OutlineInputBorder(),
  ),
  keyboardType: TextInputType.text,
  textInputAction: TextInputAction.next,
  onChanged: (value) {
    // 实时处理输入
    print('输入: $value');
  },
  onSubmitted: (value) {
    // 提交时处理
    print('提交: $value');
  },
)

TextFormField组件

TextFormField是带有验证功能的TextField:

TextFormField(
  decoration: const InputDecoration(
    labelText: '邮箱',
    hintText: '请输入邮箱地址',
  ),
  validator: (value) {
    if (value == null || value.isEmpty) {
      return '请输入邮箱';
    }
    final emailRegex = RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$');
    if (!emailRegex.hasMatch(value)) {
      return '请输入有效的邮箱地址';
    }
    return null;
  },
)

Form组件

Form组件用于管理一组表单字段:

final _formKey = GlobalKey<FormState>();

Form(
  key: _formKey,
  autovalidateMode: AutovalidateMode.onUserInteraction,
  child: Column(
    children: [
      TextFormField(
        decoration: const InputDecoration(labelText: '用户名'),
        validator: (value) {
          if (value == null || value.isEmpty) {
            return '请输入用户名';
          }
          return null;
        },
      ),
      const SizedBox(height: 16),
      ElevatedButton(
        onPressed: () {
          if (_formKey.currentState!.validate()) {
            // 表单验证通过
            print('表单验证通过');
          }
        },
        child: const Text('提交'),
      ),
    ],
  ),
)

表单验证

内置验证器

TextFormField(
  // 必填验证
  validator: (value) {
    if (value == null || value.isEmpty) {
      return '此字段必填';
    }
    return null;
  },
)

TextFormField(
  // 最小长度验证
  validator: (value) {
    if (value == null || value.length < 6) {
      return '最少需要6个字符';
    }
    return null;
  },
)

TextFormField(
  // 最大长度验证
  validator: (value) {
    if (value != null && value.length > 50) {
      return '最多允许50个字符';
    }
    return null;
  },
)

正则表达式验证

// 邮箱验证
final emailRegex = RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$');

TextFormField(
  validator: (value) {
    if (value == null || value.isEmpty) {
      return '请输入邮箱';
    }
    if (!emailRegex.hasMatch(value)) {
      return '请输入有效的邮箱地址';
    }
    return null;
  },
)

// 手机号码验证
final phoneRegex = RegExp(r'^1[3-9]\d{9}$');

TextFormField(
  keyboardType: TextInputType.phone,
  validator: (value) {
    if (value == null || value.isEmpty) {
      return '请输入手机号';
    }
    if (!phoneRegex.hasMatch(value)) {
      return '请输入有效的手机号';
    }
    return null;
  },
)

// URL验证
final urlRegex = RegExp(r'https?:\/\/[\w-]+(\.[\w-]+)+([\w.,@?^=%&:/~+#-]*[\w@?^=%&/~+#-])?');

TextFormField(
  validator: (value) {
    if (value == null || value.isEmpty) {
      return '请输入URL';
    }
    if (!urlRegex.hasMatch(value)) {
      return '请输入有效的URL';
    }
    return null;
  },
)

自定义验证器

class PasswordValidator {
  static String? validate(String? value) {
    if (value == null || value.isEmpty) {
      return '请输入密码';
    }
    
    // 检查长度
    if (value.length < 8) {
      return '密码至少需要8个字符';
    }
    
    // 检查是否包含数字
    if (!value.contains(RegExp(r'\d'))) {
      return '密码必须包含至少一个数字';
    }
    
    // 检查是否包含字母
    if (!value.contains(RegExp(r'[a-zA-Z]'))) {
      return '密码必须包含至少一个字母';
    }
    
    // 检查是否包含特殊字符
    if (!value.contains(RegExp(r'[!@#$%^&*(),.?":{}|<>]'))) {
      return '密码必须包含至少一个特殊字符';
    }
    
    return null;
  }
}

// 使用自定义验证器
TextFormField(
  obscureText: true,
  decoration: const InputDecoration(labelText: '密码'),
  validator: PasswordValidator.validate,
)

表单状态管理

使用StatefulWidget管理状态

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

  @override
  State<LoginForm> createState() => _LoginFormState();
}

class _LoginFormState extends State<LoginForm> {
  final _formKey = GlobalKey<FormState>();
  String _email = '';
  String _password = '';

  void _submitForm() {
    if (_formKey.currentState!.validate()) {
      _formKey.currentState!.save();
      // 处理表单数据
      print('邮箱: $_email, 密码: $_password');
    }
  }

  @override
  Widget build(BuildContext context) {
    return Form(
      key: _formKey,
      child: Column(
        children: [
          TextFormField(
            decoration: const InputDecoration(labelText: '邮箱'),
            validator: (value) {
              if (value == null || value.isEmpty) {
                return '请输入邮箱';
              }
              return null;
            },
            onSaved: (value) {
              _email = value ?? '';
            },
          ),
          TextFormField(
            obscureText: true,
            decoration: const InputDecoration(labelText: '密码'),
            validator: (value) {
              if (value == null || value.isEmpty) {
                return '请输入密码';
              }
              return null;
            },
            onSaved: (value) {
              _password = value ?? '';
            },
          ),
          ElevatedButton(
            onPressed: _submitForm,
            child: const Text('登录'),
          ),
        ],
      ),
    );
  }
}

使用TextEditingController

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

  @override
  State<MyForm> createState() => _MyFormState();
}

class _MyFormState extends State<MyForm> {
  final _emailController = TextEditingController();
  final _passwordController = TextEditingController();

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

  void _submit() {
    final email = _emailController.text;
    final password = _passwordController.text;
    print('邮箱: $email, 密码: $password');
  }

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        TextField(
          controller: _emailController,
          decoration: const InputDecoration(labelText: '邮箱'),
        ),
        TextField(
          controller: _passwordController,
          obscureText: true,
          decoration: const InputDecoration(labelText: '密码'),
        ),
        ElevatedButton(
          onPressed: _submit,
          child: const Text('提交'),
        ),
      ],
    );
  }
}

高级表单功能

表单联动验证

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

  @override
  State<PasswordForm> createState() => _PasswordFormState();
}

class _PasswordFormState extends State<PasswordForm> {
  final _formKey = GlobalKey<FormState>();
  final _passwordController = TextEditingController();
  final _confirmController = TextEditingController();

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

  String? _validateConfirmPassword(String? value) {
    if (value == null || value.isEmpty) {
      return '请再次输入密码';
    }
    if (value != _passwordController.text) {
      return '两次输入的密码不一致';
    }
    return null;
  }

  @override
  Widget build(BuildContext context) {
    return Form(
      key: _formKey,
      child: Column(
        children: [
          TextFormField(
            controller: _passwordController,
            obscureText: true,
            decoration: const InputDecoration(labelText: '密码'),
            validator: (value) {
              if (value == null || value.isEmpty) {
                return '请输入密码';
              }
              if (value.length < 8) {
                return '密码至少需要8个字符';
              }
              return null;
            },
          ),
          TextFormField(
            controller: _confirmController,
            obscureText: true,
            decoration: const InputDecoration(labelText: '确认密码'),
            validator: _validateConfirmPassword,
          ),
          ElevatedButton(
            onPressed: () {
              if (_formKey.currentState!.validate()) {
                print('表单验证通过');
              }
            },
            child: const Text('提交'),
          ),
        ],
      ),
    );
  }
}

动态表单字段

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

  @override
  State<DynamicForm> createState() => _DynamicFormState();
}

class _DynamicFormState extends State<DynamicForm> {
  final _formKey = GlobalKey<FormState>();
  final List<TextEditingController> _controllers = [];

  @override
  void initState() {
    super.initState();
    _controllers.add(TextEditingController());
  }

  @override
  void dispose() {
    for (var controller in _controllers) {
      controller.dispose();
    }
    super.dispose();
  }

  void _addField() {
    setState(() {
      _controllers.add(TextEditingController());
    });
  }

  void _removeField(int index) {
    setState(() {
      _controllers[index].dispose();
      _controllers.removeAt(index);
    });
  }

  void _submit() {
    if (_formKey.currentState!.validate()) {
      final values = _controllers.map((c) => c.text).toList();
      print('表单值: $values');
    }
  }

  @override
  Widget build(BuildContext context) {
    return Form(
      key: _formKey,
      child: Column(
        children: [
          for (var i = 0; i < _controllers.length; i++)
            Row(
              children: [
                Expanded(
                  child: TextFormField(
                    controller: _controllers[i],
                    decoration: InputDecoration(labelText: '字段 ${i + 1}'),
                    validator: (value) {
                      if (value == null || value.isEmpty) {
                        return '此字段必填';
                      }
                      return null;
                    },
                  ),
                ),
                IconButton(
                  icon: const Icon(Icons.remove),
                  onPressed: _controllers.length > 1 ? () => _removeField(i) : null,
                ),
              ],
            ),
          ElevatedButton(
            onPressed: _addField,
            child: const Text('添加字段'),
          ),
          ElevatedButton(
            onPressed: _submit,
            child: const Text('提交'),
          ),
        ],
      ),
    );
  }
}

异步验证

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

  @override
  State<AsyncValidationForm> createState() => _AsyncValidationFormState();
}

class _AsyncValidationFormState extends State<AsyncValidationForm> {
  final _formKey = GlobalKey<FormState>();
  final _usernameController = TextEditingController();
  bool _isChecking = false;
  String? _validationError;

  Future<void> _checkUsername() async {
    setState(() {
      _isChecking = true;
      _validationError = null;
    });

    // 模拟API调用
    await Future.delayed(const Duration(seconds: 1));
    
    // 检查用户名是否已存在
    final username = _usernameController.text;
    if (username == 'admin') {
      setState(() {
        _validationError = '用户名已存在';
      });
    }

    setState(() {
      _isChecking = false;
    });
  }

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

  @override
  Widget build(BuildContext context) {
    return Form(
      key: _formKey,
      child: Column(
        children: [
          TextFormField(
            controller: _usernameController,
            decoration: InputDecoration(
              labelText: '用户名',
              errorText: _validationError,
              suffixIcon: _isChecking 
                  ? const CircularProgressIndicator() 
                  : null,
            ),
            onChanged: (_) => _checkUsername(),
          ),
          ElevatedButton(
            onPressed: () {
              if (_formKey.currentState!.validate() && !_isChecking && _validationError == null) {
                print('表单验证通过');
              }
            },
            child: const Text('提交'),
          ),
        ],
      ),
    );
  }
}

表单样式定制

自定义输入框样式

TextField(
  decoration: InputDecoration(
    labelText: '自定义输入框',
    hintText: '请输入内容',
    labelStyle: const TextStyle(
      color: Colors.blue,
      fontSize: 16,
    ),
    hintStyle: const TextStyle(
      color: Colors.grey,
      fontSize: 14,
    ),
    border: OutlineInputBorder(
      borderRadius: BorderRadius.circular(12),
      borderSide: const BorderSide(
        color: Colors.grey,
        width: 2,
      ),
    ),
    focusedBorder: OutlineInputBorder(
      borderRadius: BorderRadius.circular(12),
      borderSide: const BorderSide(
        color: Colors.blue,
        width: 2,
      ),
    ),
    errorBorder: OutlineInputBorder(
      borderRadius: BorderRadius.circular(12),
      borderSide: const BorderSide(
        color: Colors.red,
        width: 2,
      ),
    ),
    filled: true,
    fillColor: Colors.grey[100],
    contentPadding: const EdgeInsets.all(16),
  ),
)

带图标的输入框

TextField(
  decoration: InputDecoration(
    labelText: '搜索',
    prefixIcon: const Icon(Icons.search),
    prefixIconColor: Colors.grey,
    suffixIcon: const Icon(Icons.clear),
    suffixIconColor: Colors.grey,
    border: const OutlineInputBorder(),
  ),
)

密码可见性切换

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

  @override
  State<PasswordField> createState() => _PasswordFieldState();
}

class _PasswordFieldState extends State<PasswordField> {
  bool _obscureText = true;

  @override
  Widget build(BuildContext context) {
    return TextField(
      obscureText: _obscureText,
      decoration: InputDecoration(
        labelText: '密码',
        suffixIcon: IconButton(
          icon: Icon(
            _obscureText ? Icons.visibility : Icons.visibility_off,
          ),
          onPressed: () {
            setState(() {
              _obscureText = !_obscureText;
            });
          },
        ),
        border: const OutlineInputBorder(),
      ),
    );
  }
}

实战案例

案例1:登录表单

class LoginScreen extends StatelessWidget {
  const LoginScreen({super.key});

  @override
  Widget build(BuildContext context) {
    final emailController = TextEditingController();
    final passwordController = TextEditingController();

    return Scaffold(
      appBar: AppBar(title: const Text('登录')),
      body: Padding(
        padding: const EdgeInsets.all(16),
        child: Form(
          child: Column(
            children: [
              TextFormField(
                controller: emailController,
                decoration: const InputDecoration(
                  labelText: '邮箱',
                  prefixIcon: Icon(Icons.email),
                  border: OutlineInputBorder(),
                ),
                keyboardType: TextInputType.emailAddress,
                validator: (value) {
                  if (value == null || value.isEmpty) {
                    return '请输入邮箱';
                  }
                  final emailRegex = RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$');
                  if (!emailRegex.hasMatch(value)) {
                    return '请输入有效的邮箱';
                  }
                  return null;
                },
              ),
              const SizedBox(height: 16),
              TextFormField(
                controller: passwordController,
                decoration: const InputDecoration(
                  labelText: '密码',
                  prefixIcon: Icon(Icons.lock),
                  border: OutlineInputBorder(),
                ),
                obscureText: true,
                validator: (value) {
                  if (value == null || value.isEmpty) {
                    return '请输入密码';
                  }
                  if (value.length < 6) {
                    return '密码至少6个字符';
                  }
                  return null;
                },
              ),
              const SizedBox(height: 24),
              ElevatedButton(
                onPressed: () {
                  // 处理登录逻辑
                  print('登录: ${emailController.text}');
                },
                style: ElevatedButton.styleFrom(
                  minimumSize: const Size.fromHeight(50),
                  shape: RoundedRectangleBorder(
                    borderRadius: BorderRadius.circular(12),
                  ),
                ),
                child: const Text('登录'),
              ),
            ],
          ),
        ),
      ),
    );
  }
}

案例2:注册表单

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

  @override
  State<RegisterScreen> createState() => _RegisterScreenState();
}

class _RegisterScreenState extends State<RegisterScreen> {
  final _formKey = GlobalKey<FormState>();
  final _emailController = TextEditingController();
  final _passwordController = TextEditingController();
  final _confirmController = TextEditingController();
  bool _agreed = false;

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

  void _submit() {
    if (_formKey.currentState!.validate() && _agreed) {
      // 处理注册逻辑
      print('注册成功');
    } else if (!_agreed) {
      ScaffoldMessenger.of(context).showSnackBar(
        const SnackBar(content: Text('请同意服务条款')),
      );
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('注册')),
      body: Padding(
        padding: const EdgeInsets.all(16),
        child: Form(
          key: _formKey,
          child: ListView(
            children: [
              TextFormField(
                controller: _emailController,
                decoration: const InputDecoration(
                  labelText: '邮箱',
                  border: OutlineInputBorder(),
                ),
                keyboardType: TextInputType.emailAddress,
                validator: (value) {
                  if (value == null || value.isEmpty) return '请输入邮箱';
                  if (!RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$').hasMatch(value)) {
                    return '请输入有效的邮箱';
                  }
                  return null;
                },
              ),
              const SizedBox(height: 12),
              TextFormField(
                controller: _passwordController,
                decoration: const InputDecoration(
                  labelText: '密码',
                  border: OutlineInputBorder(),
                ),
                obscureText: true,
                validator: (value) {
                  if (value == null || value.isEmpty) return '请输入密码';
                  if (value.length < 8) return '密码至少8个字符';
                  return null;
                },
              ),
              const SizedBox(height: 12),
              TextFormField(
                controller: _confirmController,
                decoration: const InputDecoration(
                  labelText: '确认密码',
                  border: OutlineInputBorder(),
                ),
                obscureText: true,
                validator: (value) {
                  if (value == null || value.isEmpty) return '请再次输入密码';
                  if (value != _passwordController.text) return '两次密码不一致';
                  return null;
                },
              ),
              const SizedBox(height: 16),
              Row(
                children: [
                  Checkbox(
                    value: _agreed,
                    onChanged: (value) {
                      setState(() {
                        _agreed = value ?? false;
                      });
                    },
                  ),
                  const Text('我已阅读并同意服务条款'),
                ],
              ),
              const SizedBox(height: 24),
              ElevatedButton(
                onPressed: _submit,
                style: ElevatedButton.styleFrom(
                  minimumSize: const Size.fromHeight(50),
                  shape: RoundedRectangleBorder(
                    borderRadius: BorderRadius.circular(12),
                  ),
                ),
                child: const Text('注册'),
              ),
            ],
          ),
        ),
      ),
    );
  }
}

表单验证最佳实践

1. 实时验证

TextFormField(
  autovalidateMode: AutovalidateMode.onUserInteraction,
  // ...
)

2. 错误信息清晰

// 好的错误信息
validator: (value) {
  if (value == null || value.isEmpty) {
    return '请输入用户名';
  }
  if (value.length < 3) {
    return '用户名至少需要3个字符';
  }
  return null;
}

// 避免模糊的错误信息
validator: (value) {
  if (value == null || value.isEmpty || value.length < 3) {
    return '输入无效'; // 不好:用户不知道具体哪里错了
  }
  return null;
}

3. 输入限制

TextField(
  maxLength: 50,
  inputFormatters: [
    FilteringTextInputFormatter.allow(RegExp(r'[a-zA-Z0-9]')),
  ],
)

4. 键盘类型适配

// 邮箱
TextField(keyboardType: TextInputType.emailAddress)

// 手机号
TextField(keyboardType: TextInputType.phone)

// 数字
TextField(keyboardType: TextInputType.number)

// 多行文本
TextField(
  keyboardType: TextInputType.multiline,
  maxLines: null,
)

5. 表单焦点管理

final _emailFocus = FocusNode();
final _passwordFocus = FocusNode();

TextField(
  focusNode: _emailFocus,
  textInputAction: TextInputAction.next,
  onSubmitted: (_) {
    FocusScope.of(context).requestFocus(_passwordFocus);
  },
)

TextField(
  focusNode: _passwordFocus,
  textInputAction: TextInputAction.done,
  onSubmitted: (_) {
    _passwordFocus.unfocus();
    // 提交表单
  },
)

总结

Flutter提供了强大的表单处理能力,通过本文的学习,你应该掌握了:

  1. 基础组件:TextField、TextFormField、Form
  2. 表单验证:内置验证、正则表达式验证、自定义验证器
  3. 状态管理:StatefulWidget、TextEditingController
  4. 高级功能:表单联动、动态字段、异步验证
  5. 样式定制:自定义输入框样式、图标、密码可见性切换

核心要点:

  • 使用Form组件管理表单状态
  • 使用TextFormField进行表单验证
  • 使用TextEditingController控制输入内容
  • 使用validator函数进行验证
  • 使用GlobalKey<FormState>管理表单

掌握这些技能后,你可以创建出功能完善、用户体验良好的表单界面。

更多推荐