1、Flutter表单基础:认识Form与FormField,构建你的第一个表单页面

表单开发,说白了就是跟用户打交道最频繁的那部分代码。登录、注册、个人信息编辑……哪个App都绕不开。我刚接触Flutter时,觉得表单不就是几个输入框加个按钮吗?后来踩了不少坑才明白——表单的难点从来不在UI,而在状态管理和校验逻辑

这一章,我们先从最基础的东西聊起。把Form和FormField这两个核心角色搞清楚,然后手把手搭出第一个表单页面。

1.1 Form与FormField:表单里的「指挥官」和「士兵」

Flutter里的表单体系,其实就两个关键角色:

  • Form:整个表单的容器,负责统筹全局。比如一键校验所有字段、一键重置数据。
  • FormField:表单里的每个具体字段。比如用户名输入框、密码输入框、下拉选择器。

你可以把Form想象成一个指挥官,FormField就是它手下的士兵。指挥官不关心士兵具体怎么打仗,但士兵打完仗要向指挥官汇报结果。

我个人习惯用GlobalKey<FormState>来关联Form,这样就能在任意地方调用formKey.currentState!.validate()来触发校验。嗯,这里要注意——currentState可能为null,所以加个!或者判空会更安全。

1.2 最简单的FormField:TextFormField

实际开发中,TextFormField是我们用得最多的FormField子类。它本质上就是TextField的增强版,自带校验和状态管理能力。

来看一个最基础的例子:

Form(
  key: _formKey,
  child: Column(
    children: [
      TextFormField(
        decoration: InputDecoration(labelText: '用户名'),
        validator: (value) {
          if (value == null || value.isEmpty) {
            return '用户名不能为空';
          }
          return null;
        },
      ),
      SizedBox(height: 16),
      ElevatedButton(
        onPressed: () {
          if (_formKey.currentState!.validate()) {
            // 校验通过,处理数据
          }
        },
        child: Text('提交'),
      ),
    ],
  ),
)

这段代码里,validator函数返回null表示校验通过,返回字符串表示错误提示。这个设计我觉得挺巧妙的——用返回值类型来区分「通过」和「不通过」,比用布尔值加错误消息两个参数要简洁。

小技巧:我在项目中遇到过一种情况——用户还没输入完就触发了校验,满屏红色提示很吓人。后来我加了个autovalidateMode: AutovalidateMode.onUserInteraction,只在用户交互后才校验,体验好很多。

1.3 自定义FormField:当内置组件不够用时

TextFormField虽然好用,但总有不够用的时候。比如你要做一个「评分组件」或者「颜色选择器」,这些没有现成的FormField子类。

这时候就需要自己动手了。Flutter提供了FormField<T>这个泛型类,你可以自定义任何类型的表单字段。

举个例子,一个简单的「是否同意协议」的复选框字段:

FormField<bool>(
  initialValue: false,
  validator: (value) {
    if (value != true) {
      return '请先同意用户协议';
    }
    return null;
  },
  builder: (FormFieldState<bool> state) {
    return Column(
      children: [
        Checkbox(
          value: state.value,
          onChanged: (value) {
            state.didChange(value);
          },
        ),
        if (state.hasError)
          Text(
            state.errorText!,
            style: TextStyle(color: Colors.red),
          ),
      ],
    );
  },
)

关键点在于state.didChange(value)——你必须手动调用这个方法,告诉Form「这个字段的值变了」。否则Form根本不知道你的字段状态,校验也就无从谈起。

曾经踩过的坑:我刚开始写自定义FormField时,忘了调didChange,结果校验永远拿不到最新值。排查了半天才发现是这个问题。记住:任何自定义字段,值变化后必须调用state.didChange()

1.4 构建你的第一个表单页面

理论说完了,我们来实战。下面是一个完整的登录表单页面,包含了用户名、密码、记住我复选框:

class LoginFormPage extends StatefulWidget {
  @override
  _LoginFormPageState createState() => _LoginFormPageState();
}

class _LoginFormPageState extends State<LoginFormPage> {
  final _formKey = GlobalKey<FormState>();
  final _usernameController = TextEditingController();
  final _passwordController = TextEditingController();
  bool _rememberMe = false;

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

  void _handleSubmit() {
    if (_formKey.currentState!.validate()) {
      // 这里可以拿到所有字段的值
      final username = _usernameController.text;
      final password = _passwordController.text;
      print('登录信息:$username, $password, 记住我:$_rememberMe');
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('登录')),
      body: Padding(
        padding: EdgeInsets.all(16),
        child: Form(
          key: _formKey,
          child: Column(
            children: [
              TextFormField(
                controller: _usernameController,
                decoration: InputDecoration(labelText: '用户名'),
                validator: (v) => v!.isEmpty ? '请输入用户名' : null,
              ),
              SizedBox(height: 12),
              TextFormField(
                controller: _passwordController,
                obscureText: true,
                decoration: InputDecoration(labelText: '密码'),
                validator: (v) => v!.length < 6 ? '密码至少6位' : null,
              ),
              FormField<bool>(
                initialValue: false,
                builder: (state) {
                  return CheckboxListTile(
                    title: Text('记住我'),
                    value: state.value,
                    onChanged: (v) {
                      state.didChange(v);
                      setState(() => _rememberMe = v!);
                    },
                  );
                },
              ),
              SizedBox(height: 24),
              ElevatedButton(
                onPressed: _handleSubmit,
                child: Text('登录'),
              ),
            ],
          ),
        ),
      ),
    );
  }
}

这个例子涵盖了本章所有知识点:

  • GlobalKey管理Form状态
  • TextFormField处理文本输入
  • 用自定义FormField<bool>处理复选框
  • validator做字段校验
核心要点:表单开发的核心就三件事——状态管理(值从哪里来)、校验逻辑(值对不对)、数据提交(值往哪里去)。把这三点理清了,任何复杂表单都不在话下。

1.5 本章小结

这一章我们认识了Form和FormField这对黄金搭档。Form负责统筹,FormField负责具体字段。内置的TextFormField能满足大部分场景,不够用时就用自定义FormField。

你想想看,表单开发其实没那么神秘。无非就是「收集数据 → 校验数据 → 提交数据」这个流程。Flutter把最繁琐的状态管理部分封装好了,我们只需要关注业务逻辑就行。

下一章,我会聊聊表单校验的进阶技巧——正则表达式、异步校验、联动校验。这些在实际项目中非常实用,到时候我会分享一些我踩过的坑和总结的经验。