如何在Flutter中创建反应式表单(附代码示例)

1,044 阅读10分钟

在您开发的几乎每一个应用程序中,迟早都会出现捕捉用户输入的需求。幸运的是,在Flutter中捕获文本输入是相当简单的。然而,随着更多的字段和输入类型被添加到一个表单中,捕获这些信息的复杂性迅速增加。

通常,这些输入字段,无论是文本字段、日期字段,还是任何其他类型的输入,都被称为 "控件"。验证也可能成为一个问题,因为即使是某些字段的简单验证也需要编写冗长的自定义验证器。

在这篇文章中,我们将创建一个具有输入验证功能的注册表单,并根据其他字段的值来改变字段。我们将首先在不使用反应式表单的情况下完成这个任务,然后使用反应式表单重新实现同一个表单,以了解Flutter中反应式表单的好处。

Flutter 反应式表单项目概述

我们将创建的应用程序是一个宠物进入 "宠物酒店 "的登记应用程序--人们在度假时可以把他们的宠物放在那里。

为了使这个应用程序发挥作用,人们需要提供一些细节,如他们的姓名和电话号码,他们有什么样的宠物,以及他们的宠物的喜好和厌恶。最终的结果会是这样的。

User Shown Entering Pet Hotel App And Entering Information In Flutter Reactive Form

这个表格有几个要求。

首先,三个后续问题必须根据用户选择的宠物类型而改变。

接下来,这三个问题的答案是必须的,所以我们必须添加Flutter表单验证逻辑,以确保它们被填写。

最后,电话号码必须只包含数字,所以如果它包含非数字值,那么表单应该拒绝该条目并通知用户。

在Flutter中制作表单,没有反应式表单

在这第一种方法中,我们自己手动创建表单,而且我们还想捕获这些单独字段中的文本输入。

正因为如此,我们要负责创建单独的TextControllers ,我们可以将其与TextFormField widgets联系起来。我们也要负责创建一个变量,以容纳所选的宠物。

现在让我们来创建这些变量:

final _formKey = GlobalKey<FormState>();
PetType? _petType;
final firstName = TextEditingController();
final lastName = TextEditingController();
final questionResponses = List.generate(3, (index) => TextEditingController());

为了将文本写入这些字段,我们将创建TextFormField 小部件,并将它们与适当的控制器绑定:

TextFormField(
  decoration: InputDecoration(hintText: 'First Name'),
  controller: firstName,
),
TextFormField(
  decoration: InputDecoration(hintText: 'Last Name'),
  controller: lastName,
),

电话号码输入字段有点不同,因为我们需要验证它是否有一个有效的电话号码,以及在检测到无效输入时提示用户:

TextFormField(
  decoration: InputDecoration(hintText: 'Phone number'),
  autovalidateMode: AutovalidateMode.always,
  validator: (val) {
    if (val == null || val == "") {
      return 'Please enter a phone number';
    }
    if (int.tryParse(val) == null) {
      return 'Only enter numbers in the phone number field';
    }
    return null;
  },
),

接下来,我们指定宠物选择器。这是一个RadioListTile ,让用户选择他们要带的宠物的种类。猫,狗,或针鼹。

当用户选择宠物的类型时,我们还想迭代以前对这些问题的回答,并清除它们,以便每次只选择一个选项:

RadioListTile<PetType>(
  value: PetType.cat,
  groupValue: _petType,
  onChanged: (val) => setState(() {
    for (final controller in questionResponses) {
      controller.clear();
    }
    _petType = val;
  }),
  title: Text('Cat'),
),

最后,我们想根据所选择的宠物类型来改变我们所问的问题。

我们可以通过使用Builder 来实现,它将根据一个给定的变量的值来更新widget树。因此,如果选择的动物类型是 "猫",表单将显示该动物类型的问题,对于狗或针鼹鼠类型的动物也是如此:

Builder(
  builder: (context) {
    switch (_petType) {
      case PetType.cat:
        return Column(
          children: [
            Text("Aw, it's a cat!"),
            PetQuestionField(question: 'Can we pat the cat?', controller: questionResponses[0]),
            PetQuestionField(question: 'Can we put a little outfit on it?', controller: questionResponses[1]),
            PetQuestionField(question: 'Does it like to jump in boxes?', controller: questionResponses[2]),
          ],
        );

      case PetType.dog:
        return Column(
          children: [
            Text("Yay, a puppy! What's its details?"),
            PetQuestionField(question: 'Can we wash your dog?', controller: questionResponses[0]),
            PetQuestionField(question: 'What is your dog\'s favourite treat?', controller: questionResponses[1]),
            PetQuestionField(question: 'Is your dog okay with other dog\'s?', controller: questionResponses[2]),
          ],
        );

      case PetType.echidna:
        return Column(
          children: [
            Text("It's a small spiky boi. Can you fill us in on some of the details?"),
            PetQuestionField(question: 'How spikey is the echidna?', controller: questionResponses[0]),
            PetQuestionField(question: 'Can we read the echidna a story?', controller: questionResponses[1]),
            PetQuestionField(question: 'Does it like leafy greens?', controller: questionResponses[2]),
          ],
        );
      case null:
        {
          return Text('Please choose your pet type from above');
        }
    }
  },
),

在创建了各个表单控件后,现在是时候为用户创建一个按钮来注册他们的宠物了。这个按钮应该只允许用户在所提供的输入是有效的情况下继续进行,并应该提示用户纠正任何不能被验证的输入:

ElevatedButton(
    onPressed: () {
      // Form is valid if the form controls are reporting that 
      // they are valid, and a pet type has been specified.
      final valid = (_formKey.currentState?.validate() ?? false) && _petType != null;
      if (!valid) {
      // If it's not valid, prompt the user to fix the form
        showDialog(
            context: context,
            builder: (context) => SimpleDialog(
                  contentPadding: EdgeInsets.all(20),
                  title: Text('Please check the form'),
                  children: [Text('Some details are missing or incorrect. Please check the details and try again.')],
                ));
      } else {
      // If it is valid, show the received values
        showDialog(
          context: context,
          builder: (context) => SimpleDialog(
            contentPadding: EdgeInsets.all(20),
            title: Text("All done!"),
            children: [
              Text(
                "Thanks for all the details! We're going to check your pet in with the following details.",
                style: Theme.of(context).textTheme.caption,
              ),
              Card(
                child: Column(
                  children: [
                    Text('First name: ${firstName.text}'),
                    Text('Last name: ${lastName.text}\r\n'),
                    Text('Pet type: ${_petType}'),
                    Text('Response 1: ${questionResponses[0].text}'),
                    Text('Response 2: ${questionResponses[1].text}'),
                    Text('Response 3: ${questionResponses[2].text}'),
                  ],
                ),
              )
            ],
          ),
        );
      }
    },
    child: Text('REGISTER'))

在 Flutter 中手动创建表单的问题

在Flutter中使用表单并不难,但手工制作我们自己的表单可能有点费劲。让我们来分析一下为什么会出现这种情况。

首先,如果你想从一个字段获取文本或清除字段的输入,你必须为每个字段创建你自己的TextEditingController 。很容易看出,你可能会有很多这样的字段,你必须自己跟踪它们。

其次,你必须为简单的事情编写自己的验证逻辑,例如检查一个数字是否正确。

最后,这种方法导致了相当多的模板代码。对于一个或两个文本字段来说,这并不坏,但很容易看出它的规模会很差。

两个反应式表单Flutter包选项可以考虑

如果我们开始寻找一个能使这一过程更容易的包,并且我们想到了 "反应式表单",我们可能会很快遇到 [reactive_forms](https://pub.dev/packages/reactive_forms)Flutter 包。然而,这并不是我用来在我的应用程序中创建反应式表单的软件包。

为什么不呢?

好吧,pub.dev上的第一句话告诉我们,反应式表单是"......一种模型驱动的方法来处理表单输入和验证,在Angular的反应式表单中受到很大启发"。

由于这一点,我们可以确定,reactive_forms 包中使用的心态将与我们在Angular中发现的类似。

如果我们已经知道Angular,那可能就更有理由使用reactive_forms 。但如果我们不知道Angular,我们更感兴趣的是在我们的表单中实现反应性的最简单方法。

根据我的经验,我发现使用包flutter_form_builder ,是一种更简单、更可扩展的创建表单的方式。

当然,我鼓励你研究这两个包并选择你喜欢的那个,因为一个包不一定比另一个 "好",但它们确实代表了实现类似结果的两种不同方式。

使用flutter_form_builder 来创建反应式表单

现在让我们使用包flutter_form_builder 来创建我们的表单。这可以减少我们要写的代码量,使我们更容易理解我们所写的代码,还可以使我们不用写自己的验证逻辑。

首先,我们要在我们的pubspec.yaml 文件中添加对flutter_form_builder 包的依赖:

flutter_form_builder: ^7.4.0

有了这个设置,让我们重新实现我们的表单以利用flutter_form_builder

我们需要为我们打算在表单中使用的字段添加一些名称。我们应该把它们设置成一个对我们来说合乎逻辑的变量名,因为我们以后需要把我们的FormBuilderTextField 与它们绑定:

final String FIRST_NAME = 'FirstName';
final String LAST_NAME = 'LastName';
final String PHONE_NUMBER = 'PhoneNumber';
final String PET_CHOICE = 'PetChoice';
final String QUESTION_ANSWER_1 = 'QuestionAnswer1';
final String QUESTION_ANSWER_2 = 'QuestionAnswer2';
final String QUESTION_ANSWER_3 = 'QuestionAnswer3';

我们还需要指定一个GlobalKey<FormBuilderState> ,来存储我们的表单所捕获的细节:

final _fbKey = GlobalKey<FormBuilderState>();

下一个大的变化是,我们的表单不是被包裹在一个Form 中,而是被包裹在一个FormBuilder 中,并为FormBuilder 指定一个键:

FormBuilder(
  key: _fbKey,
  child: Column(children: [...children widgets here])
)

这意味着FormBuilder 将把表单中的值存储在这个键中,这样我们以后就可以很容易地检索到它们了。

设置基本的表单输入

通常情况下,我们将负责手动指定什么 [TextEditingController](https://blog.logrocket.com/the-ultimate-guide-to-text-fields-in-flutter/) 应该被使用,以及手动设置验证等事项。但有了flutter_form_builder ,这两件事就变得微不足道了。

对于一个文本输入字段,我们指定该字段的name 参数,如果我们想给该字段贴上标签,则指定装饰。我们也可以直接从现有的验证器集合中选择,而不是自己编写。这意味着我们的名字和姓氏输入字段看起来像这样:

FormBuilderTextField(
  name: FIRST_NAME,
  decoration: InputDecoration(labelText: 'First Name'),
  validator: FormBuilderValidators.required(),
),

对于我们的电话号码字段,我们可以不写我们自己的验证器,而只是利用FormBuilderValidators.numeric() 验证器:

FormBuilderTextField(
  name: PHONE_NUMBER,
  validator: FormBuilderValidators.numeric(),
  decoration: InputDecoration(labelText: 'Phone number'),
  autovalidateMode: AutovalidateMode.always,
),

设置宠物类型选择器

现在我们想给用户提供一个宠物类型的选项列表,让他们在我们的Flutter应用程序中选择适当的单选按钮。我们可以通过编程方式从我们提供的枚举集中生成这个列表。

这意味着,如果我们在程序中添加或删除我们的枚举选项,这些选项也会在我们的表单中发生变化。这将比我们自己手动维护这个列表更容易:

FormBuilderRadioGroup<PetType>(
  onChanged: (val) {
    print(val);
    setState(() {
      _petType = val;
    });
  },
  name: PET_CHOICE,
  validator: FormBuilderValidators.required(),
  orientation: OptionsOrientation.vertical, // Lay out the options vertically
  options: [
    // Retrieve all options from the PetType enum and show them as options
    // Capitalize the first letters of the options as well
    ...PetType.values.map(
      (e) => FormBuilderFieldOption(
        value: e,
        child: Text(
          describeEnum(e).replaceFirst(
            describeEnum(e)[0],
            describeEnum(e)[0].toUpperCase(),
          ),
        ),
      ),
    ),
  ],
),

设置最后的三个问题

我们的构建器方法在Flutter表单的这一部分基本保持不变,但有几个重要的区别:我们现在使用FormBuilderTextField 类来输入,并通过name 参数将它们与表单中的适当条目联系起来:

case PetType.cat:
  return Column(
    children: [
      Text("Aw, it's a cat!"),
      FormBuilderTextField(
        name: QUESTION_ANSWER_1,
        decoration: InputDecoration(labelText: 'Can we pat the cat?'),
      ),
      FormBuilderTextField(
        name: QUESTION_ANSWER_2,
        decoration: InputDecoration(labelText: 'Can we put a little outfit on it?'),
      ),
      FormBuilderTextField(
        name: QUESTION_ANSWER_3,
        decoration: InputDecoration(labelText: 'Does it like to jump in boxes?'),
      ),
    ],
  );

验证和检索表单中的值

有了我们的反应式Flutter表单,现在我们需要做两件最后的事情:验证表单中是否有可用的数据,以及从表单中检索这些值。

幸运的是,由于我们已经在每个字段本身设置了验证要求,我们的验证变得非常简单:

final valid = _fbKey.currentState?.saveAndValidate() ?? false;

这个操作的结果是,如果我们表单的当前状态不是null ,并且当前被认为是valid ,也就是说,所有的表单字段都通过了验证,那么,这个表单被认为是有效的。如果currentStatenull ,或者表单是invalid ,这个变量将转而返回false

在结果成功的情况下,这些值将会显示给用户。我们可以通过访问_fbKey 对象中的currentState 对象来轻松访问表单中的值:

showDialog(
  context: context,
  builder: (context) => SimpleDialog(
    contentPadding: EdgeInsets.all(20),
    title: Text("All done!"),
    children: [
      Text(
        "Thanks for all the details! We're going to check your pet in with the following details.",
        style: Theme.of(context).textTheme.caption,
      ),
      Card(
        child: Column(
          children: [
            // It's okay to use the ! operator with currentState, because we
            // already checked that it wasn't null when we did the form
            // validation
            Text('First name: ${_fbKey.currentState!.value[FIRST_NAME]}'),
            Text('Last name: ${_fbKey.currentState!.value[LAST_NAME]}'),
            Text('Number: ${_fbKey.currentState!.value[PHONE_NUMBER]}'),
            Text('Pet type: ${_fbKey.currentState!.value[PET_CHOICE]}'),
            Text('Response 1: ${_fbKey.currentState!.value[QUESTION_ANSWER_1]}'),
            Text('Response 2: ${_fbKey.currentState!.value[QUESTION_ANSWER_2]}'),
            Text('Response 3: ${_fbKey.currentState!.value[QUESTION_ANSWER_3]}'),
          ],
        ),
      )
    ],
  ),
);

收尾工作

正如我们所看到的,在Flutter中使用flutter_form_builder 来创建反应式表单可以为我们这些开发者带来许多改进。一如既往,你可以在Github中浏览这个项目的代码,看看你如何在你的项目中使用flutter_form_builder

你也可以使用下面的这些链接,在两个提交之间进行比较,看看项目到底发生了什么变化:

flutter_form_builder 提供了相当多的不同类型的字段,所以你应该总是能够使用正确的字段类型来满足你的需求。

祝你玩得开心,并享受构建这些表单的乐趣!