Flutter for OpenHarmony二手物品置换App实战 - 表单验证实现

表单验证是保证用户输入数据有效性的重要环节,在发布商品、用户注册等场景都需要用到。今天我们来讲解"闲置换"中表单验证的实现方式。

表单验证的设计思路

好的表单验证应该在用户提交时检查所有必填项,给出明确的错误提示,引导用户正确填写。我们在发布页面实现了基本的表单验证。

发布页面的表单验证

看看发布页面的验证逻辑:

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提供了FormTextFormField来简化表单验证:

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()方法会遍历所有TextFormFieldvalidator回调,全部校验通过后执行_doPublish方法。这种方式相比手动逐个校验,更符合Flutter的组件化设计思想,简化了代码逻辑。

Form包裹所有表单字段,TextFormFieldvalidator定义验证规则。调用_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方法处理必填项校验,支持自定义提示文案,未自定义时使用默认文案。接着定义minLengthmaxLength方法,分别校验输入内容的最小、最大长度,同样支持自定义提示,将重复的长度校验逻辑抽离成通用方法,避免在多个表单字段中重复编写相同代码,提升代码复用性。

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个字'),
  ]),
)

这段代码展示了自定义验证器的使用方式,在TextFormFieldvalidator回调中,通过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

相关推荐
b2077212 小时前
Flutter for OpenHarmony 身体健康状况记录App实战 - 健康目标实现
python·flutter·harmonyos
血小板要健康2 小时前
如何计算时间复杂度(上)
java·数据结构·算法
计算机学姐2 小时前
基于SpringBoot的美食分享交流平台
java·spring boot·后端·spring·java-ee·intellij-idea·美食
Eugene__Chen2 小时前
Java关键字(曼波版)
java·开发语言
lixin5565563 小时前
基于深度生成对抗网络的高质量图像生成模型研究与实现
java·人工智能·pytorch·python·深度学习·语言模型
龚礼鹏3 小时前
图像显示框架八——BufferQueue与BLASTBufferQueue(基于android 15源码分析)
android·c语言
代码雕刻家3 小时前
4.3.多线程&JUC-多线程的实现方式
java·开发语言
Knight_AL3 小时前
Spring Boot 事件机制详解:原理 + Demo
java·数据库·spring boot
李少兄3 小时前
Java 后端开发中 Service 层依赖注入的最佳实践:Mapper 还是其他 Service?
java·开发语言