在您开发的几乎每一个应用程序中,迟早都会出现捕捉用户输入的需求。幸运的是,在Flutter中捕获文本输入是相当简单的。然而,随着更多的字段和输入类型被添加到一个表单中,捕获这些信息的复杂性迅速增加。
通常,这些输入字段,无论是文本字段、日期字段,还是任何其他类型的输入,都被称为 "控件"。验证也可能成为一个问题,因为即使是某些字段的简单验证也需要编写冗长的自定义验证器。
在这篇文章中,我们将创建一个具有输入验证功能的注册表单,并根据其他字段的值来改变字段。我们将首先在不使用反应式表单的情况下完成这个任务,然后使用反应式表单重新实现同一个表单,以了解Flutter中反应式表单的好处。
Flutter 反应式表单项目概述
我们将创建的应用程序是一个宠物进入 "宠物酒店 "的登记应用程序--人们在度假时可以把他们的宠物放在那里。
为了使这个应用程序发挥作用,人们需要提供一些细节,如他们的姓名和电话号码,他们有什么样的宠物,以及他们的宠物的喜好和厌恶。最终的结果会是这样的。

这个表格有几个要求。
首先,三个后续问题必须根据用户选择的宠物类型而改变。
接下来,这三个问题的答案是必须的,所以我们必须添加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 ,也就是说,所有的表单字段都通过了验证,那么,这个表单被认为是有效的。如果currentState 是null ,或者表单是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 提供了相当多的不同类型的字段,所以你应该总是能够使用正确的字段类型来满足你的需求。
祝你玩得开心,并享受构建这些表单的乐趣!