flutter 开发笔记(五):表单

385 阅读5分钟

在移动应用开发中,表单是一个非常常见的用户界面元素,它允许用户输入和提交信息。在这篇文章中,我们将介绍如何在 Flutter 中使用表单

需求

我们将从一个基本的用户登录表单开始,该表单包含电子邮件输入框和密码输入框。我们还将添加一个提交按钮来提交表单

示例代码(延迟验证)

import 'package:flutter/material.dart';

class FormPage extends StatefulWidget {
  const FormPage({super.key});
  @override
  FormPageState createState() => FormPageState();
}

class FormPageState extends State<FormPage> {
  final _formKey = GlobalKey<FormState>();
  final _emailController = TextEditingController();
  final _passwordController = TextEditingController();
  bool _obscureText = true;
  void _togglePasswordVisibility() {
    setState(() {
      _obscureText = !_obscureText;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Form Page'),
      ),
      body: Padding(
        padding: const EdgeInsets.all(16.0),
        child: Form(
          key: _formKey,
          child: Column(
            children: [
              TextFormField(
                controller: _emailController,
                decoration: const InputDecoration(labelText: 'Email'),
                validator: (value) {
                  if (value == null || value.isEmpty) {
                    return 'Please enter your email';
                  }
                  if (!RegExp(r'^[^@]+@[^@]+\.[^@]+').hasMatch(value)) {
                    return 'Please enter a valid email';
                  }
                  return null;
                },
              ),
              TextFormField(
                controller: _passwordController,
                decoration: InputDecoration(
                  labelText: 'Password',
                  suffixIcon: IconButton(
                    icon: Icon(
                      _obscureText ? Icons.visibility : Icons.visibility_off,
                    ),
                    onPressed: _togglePasswordVisibility,
                  ),
                ),
                obscureText: _obscureText,
                validator: (value) {
                  if (value == null || value.isEmpty) {
                    return 'Please enter your password';
                  }
                  return null;
                },
              ),
              const SizedBox(height: 20),
              ElevatedButton(
                onPressed: () {
                  if (_formKey.currentState?.validate() ?? false) {
                    ScaffoldMessenger.of(context).showSnackBar(
                      const SnackBar(content: Text('Form submitted')),
                    );
                    // 表单验证成功,打印电子邮件和密码
                    print('Email: ${_emailController.text}');
                    print('Password: ${_passwordController.text}');
                  }
                },
                child: const Text('Submit'),
              ),
            ],
          ),
        ),
      ),
    );
  }

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

详细解释

  1. 创建 State 类:

    • 我们首先创建一个继承自 StateFormPageState 类,这个类负责管理表单的状态。
    • 我们使用 GlobalKey<FormState> 来管理表单的状态,这样可以方便地进行表单验证。
  2. 文本控制器:

    • 使用 TextEditingController 控制两个文本输入框,一个用于电子邮件,另一个用于密码。
    • 在表单提交时,我们可以很方便地获取用户输入的值。
  3. 表单验证:

    • 使用 validator 参数对输入的电子邮件和密码进行验证。
    • 电子邮件输入框的验证规则包括检查是否为空以及是否符合电子邮件格式的正则表达式。
    • 密码输入框的验证规则仅检查是否为空。
  4. 密码可见性切换:

    • 使用一个布尔变量 _obscureText 来控制密码输入框的可见性。
    • 当用户点击密码输入框右侧的图标时,调用 _togglePasswordVisibility 方法切换密码的可见性。
  5. 表单提交:

    • 当用户点击提交按钮时,首先调用 validate 方法验证表单。
    • 如果表单验证通过,使用 ScaffoldMessenger 显示一条成功提交的提示信息。
  6. 资源清理:

    • dispose 方法中,释放文本控制器所占用的资源。

即时验证

上述的代码是延迟验证(Deferred Validation),这种方式是在用户提交表单时才进行验证。用户点击提交按钮后,验证逻辑才会执行,如果存在错误,会显示错误提示信息。这种方式在用户输入阶段不会提供实时反馈,但在提交时确保所有输入项都被验证

还有另外一种验证方式即时验证(Immediate Validation),这种方式是在用户输入内容时实时进行验证。每当输入内容发生变化时,验证逻辑立即执行,并更新错误提示信息。这种方式提供了即时反馈,可以帮助用户立即纠正输入错误

接下来,改造上面的代码,采用即时验证,当验证不通过时,button 处于 disable 状态,只有全部验证通过,才 enable;可以看到,核心是 onChanged 回调函数,我们使用 setState 来更新错误信息并验证表单的有效性

import 'package:flutter/material.dart';

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

  @override
  FormPageState createState() => FormPageState();
}

class FormPageState extends State<FormPage> {
  final _formKey = GlobalKey<FormState>();
  final _emailController = TextEditingController();
  final _passwordController = TextEditingController();
  bool _obscureText = true;
  bool _isButtonDisabled = true;
  String? _emailError;
  String? _passwordError;

  void _togglePasswordVisibility() {
    setState(() {
      _obscureText = !_obscureText;
    });
  }

  void _validateForm() {
    final isEmailValid = _emailController.text.isNotEmpty &&
        RegExp(r'^[^@]+@[^@]+\.[^@]+').hasMatch(_emailController.text);
    final isPasswordValid = _passwordController.text.isNotEmpty;
    setState(() {
      _isButtonDisabled = !(isEmailValid && isPasswordValid);
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Form Page'),
      ),
      body: Padding(
        padding: const EdgeInsets.all(16.0),
        child: Form(
          key: _formKey,
          child: Column(
            children: [
              TextFormField(
                controller: _emailController,
                decoration:
                    InputDecoration(labelText: 'Email', errorText: _emailError),
                onChanged: (value) {
                  setState(() {
                    if (value.isEmpty) {
                      _emailError = 'Please enter your email';
                    } else if (!RegExp(r'^[^@]+@[^@]+\.[^@]+')
                        .hasMatch(value)) {
                      _emailError = 'Please enter a valid email';
                    } else {
                      _emailError = null;
                    }
                  });
                  _validateForm();
                },
              ),
              TextFormField(
                controller: _passwordController,
                decoration: InputDecoration(
                    labelText: 'Password',
                    suffixIcon: IconButton(
                      icon: Icon(
                        _obscureText ? Icons.visibility : Icons.visibility_off,
                      ),
                      onPressed: _togglePasswordVisibility,
                    ),
                    errorText: _passwordError),
                obscureText: _obscureText,
                onChanged: (value) {
                  setState(() {
                    if (value.isEmpty) {
                      _passwordError = 'Please enter your password';
                    } else {
                      _passwordError = null;
                    }
                  });
                  _validateForm();
                },
              ),
              const SizedBox(height: 20),
              ElevatedButton(
                onPressed: _isButtonDisabled
                    ? null
                    : () {
                        if (_formKey.currentState?.validate() ?? false) {
                          ScaffoldMessenger.of(context).showSnackBar(
                            const SnackBar(content: Text('Form submitted')),
                          );
                          // 表单验证成功,打印电子邮件和密码
                          print('Email: ${_emailController.text}');
                          print('Password: ${_passwordController.text}');
                        }
                      },
                child: const Text('Submit'),
              ),
            ],
          ),
        ),
      ),
    );
  }

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

其他常用表单控件

除了上面提到的 TextFormField 组件外,DropdownButtonFormFieldCheckbox 也是出镜率比较高的,现在再加上这两种控件

import 'package:flutter/material.dart';

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

  @override
  FormPageState createState() => FormPageState();
}

class FormPageState extends State<FormPage> {
  final _formKey = GlobalKey<FormState>();
  final _emailController = TextEditingController();
  final _passwordController = TextEditingController();
  bool _obscureText = true;
  bool _agreeToTerms = false;
  String? _termsError;
  String? _selectedRole;
  final List<String> _roles = ['User', 'Admin'];

  void _togglePasswordVisibility() {
    setState(() {
      _obscureText = !_obscureText;
    });
  }

  void _updateTermsError() {
    if (!_agreeToTerms) {
      setState(() {
        _termsError = 'You must agree to the terms and conditions';
      });
    } else {
      setState(() {
        _termsError = null;
      });
    }
  }

  void _submitForm() {
    _updateTermsError();
    if (_formKey.currentState?.validate() ?? false) {
      if (_agreeToTerms) {
        ScaffoldMessenger.of(context).showSnackBar(
          const SnackBar(content: Text('Form submitted')),
        );
        // 表单验证成功,打印电子邮件和密码
        print('Email: ${_emailController.text}');
        print('Password: ${_passwordController.text}');
        print('Role: $_selectedRole');
      }
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Form Page'),
      ),
      body: Padding(
        padding: const EdgeInsets.all(16.0),
        child: Form(
          key: _formKey,
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.start,
            children: [
              TextFormField(
                controller: _emailController,
                decoration: const InputDecoration(labelText: 'Email'),
                validator: (value) {
                  if (value == null || value.isEmpty) {
                    return 'Please enter your email';
                  }
                  if (!RegExp(r'^[^@]+@[^@]+\.[^@]+').hasMatch(value)) {
                    return 'Please enter a valid email';
                  }
                  return null;
                },
              ),
              TextFormField(
                controller: _passwordController,
                decoration: InputDecoration(
                  labelText: 'Password',
                  suffixIcon: IconButton(
                    icon: Icon(
                      _obscureText ? Icons.visibility : Icons.visibility_off,
                    ),
                    onPressed: _togglePasswordVisibility,
                  ),
                ),
                obscureText: _obscureText,
                validator: (value) {
                  if (value == null || value.isEmpty) {
                    return 'Please enter your password';
                  }
                  return null;
                },
              ),
              const SizedBox(height: 20),
              DropdownButtonFormField<String>(
                value: _selectedRole,
                hint: const Text('Select a role'),
                items: _roles.map((String role) {
                  return DropdownMenuItem<String>(
                    value: role,
                    child: Text(role),
                  );
                }).toList(),
                onChanged: (newValue) {
                  setState(() {
                    _selectedRole = newValue;
                  });
                },
                validator: (value) {
                  if (value == null || value.isEmpty) {
                    return 'Please select a role';
                  }
                  return null;
                },
              ),
              const SizedBox(height: 20),
              Row(
                children: [
                  Checkbox(
                    value: _agreeToTerms,
                    onChanged: (value) {
                      setState(() {
                        _agreeToTerms = value ?? false;
                        if (_agreeToTerms) {
                          _termsError = null;
                        }
                      });
                    },
                  ),
                  const Text('I agree to the terms and conditions'),
                ],
              ),
              if (_termsError != null)
                Padding(
                  padding: const EdgeInsets.only(bottom: 16.0),
                  child: Text(
                    _termsError!,
                    style: const TextStyle(color: Colors.red),
                  ),
                ),
              ElevatedButton(
                onPressed: _submitForm,
                child: const Text('Submit'),
              ),
            ],
          ),
        ),
      ),
    );
  }

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

可以看到,DropdownButtonFormField 是可以和 Form 很好地结合起来,用法也和 TextFormField 基本一致,稍显麻烦的是 Checkbox,需要我们手动触发错误