
表单验证是保证用户输入数据有效性的重要环节,在发布商品、用户注册等场景都需要用到。今天我们来讲解"闲置换"中表单验证的实现方式。
表单验证的设计思路
好的表单验证应该在用户提交时检查所有必填项,给出明确的错误提示,引导用户正确填写。我们在发布页面实现了基本的表单验证。
发布页面的表单验证
看看发布页面的验证逻辑:
dart
class _PublishPageState extends State<PublishPage> {
final TextEditingController _titleController = TextEditingController();
final TextEditingController _descController = TextEditingController();
final TextEditingController _priceController = TextEditingController();
final TextEditingController _originalPriceController = TextEditingController();
String _selectedCategory = '数码';
final List<String> _images = [];
void _publish() {
if (_titleController.text.isEmpty) {
Get.snackbar('提示', '请输入标题', snackPosition: SnackPosition.BOTTOM);
return;
}
以上代码首先定义了发布页面的状态类,初始化了标题、描述、售价、原价等输入框的控制器,同时声明了商品分类和图片列表变量。_publish方法作为核心验证入口,首先校验标题输入框是否为空,若为空则通过底部Snackbar提示用户,并用return终止后续逻辑,避免无效提交。
dart
if (_priceController.text.isEmpty) {
Get.snackbar('提示', '请输入售价', snackPosition: SnackPosition.BOTTOM);
return;
}
Get.snackbar('成功', '发布成功', snackPosition: SnackPosition.BOTTOM);
Get.back();
}
}
这段代码是基础验证逻辑的收尾部分,继标题校验后,进一步检查售价输入框是否为空,同样通过底部提示反馈用户。若两项必填项都验证通过,则弹出发布成功提示并返回上一级页面,完成基础的发布流程闭环。这里的SnackPosition设置为BOTTOM,是为了避免提示框遮挡输入区域,提升用户操作体验。
_publish方法在用户点击发布按钮时调用,依次检查标题和售价是否为空。如果为空就用Get.snackbar显示提示,return阻止后续代码执行。
snackPosition: SnackPosition.BOTTOM让提示从底部弹出,不会遮挡输入区域。
更完善的验证逻辑
实际项目中验证逻辑应该更完善:
dart
void _publish() {
// 验证图片
if (_images.isEmpty) {
Get.snackbar('提示', '请至少添加一张图片', snackPosition: SnackPosition.BOTTOM);
return;
}
// 验证标题
if (_titleController.text.isEmpty) {
Get.snackbar('提示', '请输入标题', snackPosition: SnackPosition.BOTTOM);
return;
}
if (_titleController.text.length < 5) {
Get.snackbar('提示', '标题至少5个字', snackPosition: SnackPosition.BOTTOM);
return;
}
if (_titleController.text.length > 30) {
Get.snackbar('提示', '标题最多30个字', snackPosition: SnackPosition.BOTTOM);
return;
}
// 验证描述
if (_descController.text.isEmpty) {
Get.snackbar('提示', '请输入描述', snackPosition: SnackPosition.BOTTOM);
return;
}
这段代码开始拓展验证逻辑,首先新增图片校验,确保用户至少上传一张商品图片,这是二手物品发布的基础要求。接着对标题进行多维度校验,除了空值检查,还限制了标题长度在5-30字之间,既避免标题过短信息不全,也防止过长影响展示效果。最后加入描述的空值校验,保证商品有基础的信息说明。
dart
if (_descController.text.length < 10) {
Get.snackbar('提示', '描述至少10个字', snackPosition: SnackPosition.BOTTOM);
return;
}
// 验证价格
if (_priceController.text.isEmpty) {
Get.snackbar('提示', '请输入售价', snackPosition: SnackPosition.BOTTOM);
return;
}
final price = double.tryParse(_priceController.text);
if (price == null || price <= 0) {
Get.snackbar('提示', '请输入有效的售价', snackPosition: SnackPosition.BOTTOM);
return;
}
if (price > 999999) {
Get.snackbar('提示', '售价不能超过999999', snackPosition: SnackPosition.BOTTOM);
return;
}
// 验证原价(可选,但如果填了要验证)
if (_originalPriceController.text.isNotEmpty) {
此部分代码先补充描述的长度校验,要求描述至少10个字,确保商品信息足够详细。然后针对售价做精细化校验,除空值检查外,还通过double.tryParse尝试转换输入内容,校验是否为有效数字,同时限制售价大于0且不超过999999,覆盖价格格式和范围的合法性。最后开始处理原价的校验逻辑,因原价为可选项,故先判断是否有输入再执行后续验证。
dart
final originalPrice = double.tryParse(_originalPriceController.text);
if (originalPrice == null || originalPrice <= 0) {
Get.snackbar('提示', '请输入有效的原价', snackPosition: SnackPosition.BOTTOM);
return;
}
if (originalPrice < price) {
Get.snackbar('提示', '原价不能低于售价', snackPosition: SnackPosition.BOTTOM);
return;
}
}
// 验证通过,执行发布
_doPublish();
}
这段代码完成原价的校验逻辑,先校验输入的原价是否为有效正数,再判断原价是否低于售价,符合二手物品定价的常识逻辑,避免出现不合理的价格设置。所有验证项通过后,调用_doPublish方法执行实际的发布操作,将验证逻辑与业务逻辑解耦,提升代码可读性和维护性。
验证内容包括:图片至少一张、标题长度限制、描述长度限制、价格格式和范围、原价和售价的关系等。
使用Form和TextFormField
Flutter提供了Form和TextFormField来简化表单验证:
dart
class _PublishPageState extends State<PublishPage> {
final _formKey = GlobalKey<FormState>();
final _titleController = TextEditingController();
final _priceController = TextEditingController();
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('发布闲置'),
actions: [
TextButton(
onPressed: _submit,
child: const Text('发布'),
),
],
),
这里引入Flutter原生的表单验证组件,首先初始化GlobalKey<FormState>用于标识表单并触发验证,同时保留标题和售价的输入控制器。在页面构建方法中,搭建基础的Scaffold结构,AppBar设置页面标题,并添加发布按钮绑定_submit方法,作为表单提交的触发入口。
dart
body: Form(
key: _formKey,
child: SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(
children: [
TextFormField(
controller: _titleController,
decoration: const InputDecoration(
labelText: '标题',
hintText: '给宝贝起个名字',
),
validator: (value) {
if (value == null || value.isEmpty) {
return '请输入标题';
}
此段代码用Form组件包裹整个表单区域,并绑定之前定义的_formKey。使用SingleChildScrollView避免键盘弹出时页面溢出,Column用于纵向排列表单字段。第一个TextFormField对应标题输入框,配置了输入控制器、显示样式,核心的validator回调定义了标题的空值校验规则,校验失败时返回的字符串会自动显示在输入框下方。
dart
if (value.length < 5) {
return '标题至少5个字';
}
return null;
},
),
const SizedBox(height: 16),
TextFormField(
controller: _priceController,
decoration: const InputDecoration(
labelText: '售价',
prefixText: '¥ ',
),
keyboardType: TextInputType.number,
validator: (value) {
if (value == null || value.isEmpty) {
return '请输入售价';
}
这段代码补充标题的长度校验规则,要求标题至少5个字,校验通过则返回null。通过SizedBox设置字段间距,提升UI美观度。接着定义售价的TextFormField,配置人民币符号前缀、数字键盘类型,validator回调先校验售价是否为空,后续会补充格式校验。
dart
final price = double.tryParse(value);
if (price == null || price <= 0) {
return '请输入有效的售价';
}
return null;
},
),
],
),
),
),
);
}
void _submit() {
if (_formKey.currentState!.validate()) {
// 验证通过
_doPublish();
}
}
}
此部分完成售价输入框的校验逻辑,尝试将输入内容转换为浮点数,校验是否为有效正数,确保售价格式合法。_submit方法是表单提交的核心方法,通过_formKey触发整个表单的校验,validate()方法会遍历所有TextFormField的validator回调,全部校验通过后执行_doPublish方法。这种方式相比手动逐个校验,更符合Flutter的组件化设计思想,简化了代码逻辑。
Form包裹所有表单字段,TextFormField的validator定义验证规则。调用_formKey.currentState!.validate()会触发所有字段的验证,如果有错误会在字段下方显示错误信息。
实时验证
可以在用户输入时实时验证,而不是等到提交时:
dart
TextFormField(
controller: _titleController,
decoration: const InputDecoration(
labelText: '标题',
),
autovalidateMode: AutovalidateMode.onUserInteraction,
validator: (value) {
if (value == null || value.isEmpty) {
return '请输入标题';
}
return null;
},
)
这段代码实现了表单的实时验证功能,核心是为TextFormField添加autovalidateMode: AutovalidateMode.onUserInteraction配置,该配置会在用户与输入框交互(输入、删除内容等)后自动触发validator校验。相比提交时统一校验,实时验证能即时反馈输入错误,引导用户边输入边修正,大幅提升表单填写的用户体验,尤其适合对输入格式、长度有明确要求的字段。
autovalidateMode: AutovalidateMode.onUserInteraction让字段在用户交互后自动验证,输入内容后立即显示验证结果。
自定义验证器
可以封装常用的验证规则:
dart
class Validators {
static String? required(String? value, {String? message}) {
if (value == null || value.isEmpty) {
return message ?? '此项为必填';
}
return null;
}
static String? minLength(String? value, int min, {String? message}) {
if (value != null && value.length < min) {
return message ?? '至少$min个字符';
}
return null;
}
static String? maxLength(String? value, int max, {String? message}) {
if (value != null && value.length > max) {
return message ?? '最多$max个字符';
}
return null;
}
这里封装了通用的验证器类Validators,首先定义required方法处理必填项校验,支持自定义提示文案,未自定义时使用默认文案。接着定义minLength和maxLength方法,分别校验输入内容的最小、最大长度,同样支持自定义提示,将重复的长度校验逻辑抽离成通用方法,避免在多个表单字段中重复编写相同代码,提升代码复用性。
dart
static String? isNumber(String? value, {String? message}) {
if (value != null && value.isNotEmpty) {
if (double.tryParse(value) == null) {
return message ?? '请输入有效的数字';
}
}
return null;
}
static String? isPositive(String? value, {String? message}) {
if (value != null && value.isNotEmpty) {
final number = double.tryParse(value);
if (number == null || number <= 0) {
return message ?? '请输入大于0的数字';
}
}
return null;
}
static String? compose(String? value, List<String? Function(String?)> validators) {
for (final validator in validators) {
final result = validator(value);
if (result != null) {
return result;
}
}
return null;
}
}
这段代码继续拓展Validators类,新增isNumber方法校验输入是否为有效数字,isPositive方法校验是否为正数,覆盖价格类字段的核心校验需求。最关键的compose方法支持组合多个验证器,遍历传入的验证器列表,依次执行校验,只要有一个校验失败就返回对应的错误提示,全部通过则返回null。该方法实现了验证规则的灵活组合,可根据不同字段的需求,自由搭配必填、长度、格式等校验规则。
使用时:
dart
TextFormField(
validator: (value) => Validators.compose(value, [
(v) => Validators.required(v, message: '请输入标题'),
(v) => Validators.minLength(v, 5, message: '标题至少5个字'),
(v) => Validators.maxLength(v, 30, message: '标题最多30个字'),
]),
)
这段代码展示了自定义验证器的使用方式,在TextFormField的validator回调中,通过Validators.compose组合三个验证规则:必填校验、最小长度5字、最大长度30字,并为每个规则自定义提示文案。这种方式将校验逻辑与UI组件解耦,表单字段只需关注使用哪些验证规则,无需关心规则的具体实现,大幅提升代码的可维护性,尤其适合多字段、多规则的复杂表单场景。
compose方法组合多个验证器,依次执行直到遇到错误。
表单状态管理
复杂表单可以用状态管理来处理:
dart
class PublishController extends GetxController {
final titleController = TextEditingController();
final priceController = TextEditingController();
var images = <String>[].obs;
var selectedCategory = '数码'.obs;
var isSubmitting = false.obs;
String? validateTitle() {
final value = titleController.text;
if (value.isEmpty) return '请输入标题';
if (value.length < 5) return '标题至少5个字';
return null;
}
这里采用GetX状态管理方案,定义PublishController继承GetxController,统一管理发布表单的状态和逻辑。首先初始化输入控制器、响应式的图片列表、商品分类和提交状态变量,obs标识的变量支持响应式更新,变量变化时UI会自动刷新。validateTitle方法抽离标题的校验逻辑,返回错误提示或null,将校验逻辑从UI层迁移到控制器层,符合MVVM架构的设计思想。
dart
String? validatePrice() {
final value = priceController.text;
if (value.isEmpty) return '请输入售价';
final price = double.tryParse(value);
if (price == null || price <= 0) return '请输入有效的售价';
return null;
}
bool validate() {
final errors = [
validateTitle(),
validatePrice(),
].where((e) => e != null).toList();
if (errors.isNotEmpty) {
Get.snackbar('提示', errors.first!);
return false;
}
return true;
}
这段代码新增validatePrice方法处理售价校验,然后定义validate方法整合所有字段的校验逻辑,将各字段的校验结果收集到列表中,过滤出非空的错误提示。若存在错误,通过Snackbar显示第一个错误提示并返回false;若全部校验通过则返回true。这种方式统一了校验入口,便于后续维护和拓展更多字段的校验规则。
dart
Future<void> submit() async {
if (!validate()) return;
isSubmitting.value = true;
try {
await api.publishProduct(/* ... */);
Get.back();
Get.snackbar('成功', '发布成功');
} catch (e) {
Get.snackbar('错误', '发布失败,请重试');
} finally {
isSubmitting.value = false;
}
}
}
此段代码实现表单的提交逻辑,submit方法先调用validate方法做前置校验,校验失败则直接返回。通过isSubmitting响应式变量标记提交状态,避免用户重复点击发布按钮。在try块中调用接口执行发布操作,成功则返回上一页并提示发布成功;失败则捕获异常并提示错误。finally块确保无论发布成功或失败,都将提交状态重置为false,保证UI状态的正确性。
把验证逻辑放在Controller里,页面只负责UI展示。
小结
这篇讲解了"闲置换"App中表单验证的实现方式,包括基本的空值检查、使用Form和TextFormField、实时验证、自定义验证器、状态管理等。好的表单验证能防止无效数据提交,提升用户体验。
欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net