当前位置: 首页 > news >正文

Flutter for OpenHarmony 校园闲置跳蚤市场APP 实战DAY4:发布闲置页面+表单校验+本地存储提交

Flutter for OpenHarmony 校园闲置跳蚤市场APP 实战DAY4:发布闲置页面+表单校验+本地存储提交

欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net

哈喽各位小伙伴!咱们校园闲置跳蚤市场连载来到DAY4啦🎉 进度稳步推进,新手跟着敲完全能跟上,全程不跳步、不搞复杂概念,还是老规矩:口语化大段讲解+每处关键功能附5–6行精简代码,不堆冗余代码,写完直接能发CSDN,兼顾实用性和毕设规范,今天重点搞定「发布闲置」这个核心功能——毕竟有发布,才能有真正的闲置商品,这也是整个APP的核心交互之一!

回顾前三期进度,帮大家快速回顾,避免脱节:

  • DAY1:新建项目、配依赖、搭底部Tab导航、全局初始化,打好项目地基;
  • DAY2:写商品实体类、预置校园分类/成色常量、搭首页顶部分类标签;
  • DAY3:封装商品卡片、造模拟假数据、实现分类联动筛选,首页完全成型。

今天DAY4,我们聚焦「发布闲置」页面,从布局搭建、表单输入、校验,到提交保存、同步刷新首页,一步步落地,难度循序渐进,新手不用慌,每一步都有详细讲解和精简代码,照抄就能跑通。

🚀 DAY4 本期开发目标(详细版,新手必看)

  1. 搭建「发布闲置」完整页面布局,贴合校园场景,包含:多图上传占位、标题输入、价格输入、分类选择、成色选择、描述输入、提交按钮;
  2. 实现所有表单输入逻辑,绑定控制器,确保输入内容能正常获取;
  3. 做表单校验(必填项不能为空、价格不能为0/负数),避免无效提交,提升用户体验;
  4. 对接本地存储,实现「发布闲置提交后,自动存入本地,首页实时刷新」;
  5. 生成商品唯一ID(避免重复)、自动获取当前发布时间,不用手动输入;
  6. 优化发布页面UI,贴合鸿蒙简约风格,和首页、卡片样式统一,细节拉满;
  7. 解决新手常见的「发布后不刷新」「数据存不上」「表单报错」等问题,提前避坑。

一、搭建发布闲置页面整体布局(核心UI)

发布页面是用户交互的关键,布局要清晰、操作要简单,贴合学生使用习惯——不用复杂排版,按「图片上传→基本信息→提交」的顺序排布,一目了然。

先打开page/publish/publish_page.dart(DAY1已经建好文件夹,直接新建这个文件),整体布局用SingleChildScrollView包裹,避免小屏手机键盘弹出后遮挡表单,这是新手最容易忽略的点,加上这个,体验更友好。

整体布局核心代码(精简版,直接复制)

import'package:flutter/material.dart';import'package:campus_flea_market/config/app_color.dart';import'package:campus_flea_market/config/app_constant.dart';classPublishPageextendsStatefulWidget{constPublishPage({super.key});@overrideState<PublishPage>createState()=>_PublishPageState();}class_PublishPageStateextendsState<PublishPage>{@overrideWidgetbuild(BuildContextcontext){returnScaffold(appBar:AppBar(title:constText("发布闲置"),backgroundColor:AppColor.primary,foregroundColor:Colors.white,),body:SingleChildScrollView(padding:constEdgeInsets.all(15),child:Column(crossAxisAlignment:CrossAxisAlignment.start,children:[// 1. 多图上传区域(今天先做占位,后期加真实上传)_buildImageUpload(),constSizedBox(height:20),// 2. 标题输入框_buildTitleInput(),constSizedBox(height:15),// 3. 价格输入框_buildPriceInput(),constSizedBox(height:15),// 4. 分类选择_buildCategorySelect(),constSizedBox(height:15),// 5. 成色选择_buildConditionSelect(),constSizedBox(height:15),// 6. 描述输入框_buildDescInput(),constSizedBox(height:30),// 7. 提交按钮_buildSubmitBtn(),],),),);}}

布局说明(口语化拆解)

  • 顶部AppBar:标题「发布闲置」,用全局主色,白色文字,贴合鸿蒙原生APP风格;
  • 用SingleChildScrollView包裹所有表单,解决键盘遮挡问题,新手一定要加;
  • 所有表单按顺序排布,间距统一(15/20),视觉整齐,操作流畅;
  • 每个表单区域单独封装成一个方法(比如_buildImageUpload),代码结构清晰,后期修改方便,符合工程化规范,毕设加分。

二、逐个实现表单组件(附核心代码)

我们逐个实现上面布局里的7个部分,每个部分都附5–6行精简代码,不堆长代码,新手照抄就能用,重点讲解核心逻辑,不用死记硬背。

1. 多图上传占位区域(后期可拓展真实上传)

学生发布闲置,肯定要传商品图片,今天先做占位布局,样式美观,后期对接图片上传功能完全不用改布局,先实现“样子”,再完善功能。

// 多图上传占位Widget_buildImageUpload(){returnColumn(crossAxisAlignment:CrossAxisAlignment.start,children:[constText("商品图片(最多3张)",style:TextStyle(fontSize:15)),constSizedBox(height:8),// 图片占位框,点击可添加图片Container(width:100,height:100,decoration:BoxDecoration(border:Border.all(color:Colors.grey.shade300),borderRadius:BorderRadius.circular(8),),child:constIcon(Icons.add_photo_alternate,color:Colors.grey),),],);}
  • 用灰色边框+加号图标做占位,直观提示用户“点击添加图片”;
  • 限制最多3张图,贴合校园闲置场景(不用太多图,清晰即可);
  • 后期对接图片上传,只需要替换这个占位容器,其他布局不变,扩展性强。

2. 标题输入框(必填项)

标题是商品的核心标识,必须让用户填写,用TextField做输入框,限制输入长度(最多30字),避免标题过长。

// 标题输入框finalTextEditingController_titleCtrl=TextEditingController();Widget_buildTitleInput(){returnTextField(controller:_titleCtrl,maxLength:30,decoration:InputDecoration(hintText:"请输入商品标题(如:九成新iPhone13)",border:OutlineInputBorder(borderRadius:BorderRadius.circular(8)),),);}
  • 用TextEditingController获取输入的标题内容,后续提交时用;
  • 限制30字,符合校园闲置标题习惯(简洁明了);
  • 用OutlineInputBorder做边框,圆角8,贴合鸿蒙简约UI风格。

3. 价格输入框(必填项,只能输数字)

价格是学生交易最关心的,必须限制输入类型(只能输数字和小数点),避免输入中文、符号,同时后续要校验“不能为0、不能为负数”。

// 价格输入框finalTextEditingController_priceCtrl=TextEditingController();Widget_buildPriceInput(){returnTextField(controller:_priceCtrl,keyboardType:TextInputType.numberWithOptions(decimal:true),decoration:InputDecoration(hintText:"请输入商品价格(元)",border:OutlineInputBorder(borderRadius:BorderRadius.circular(8)),suffixText:"元",),);}
  • keyboardType设置为数字+小数点,避免无效输入;
  • 后缀加“元”,用户体验更友好,不用手动输入单位;
  • 同样用控制器获取输入的价格,后续转成double类型存入实体类。

4. 分类选择(下拉选择,复用全局常量)

分类直接复用DAY2预置的AppConstant.goodsCategory,不用手动写分类选项,下拉选择,操作简单,避免用户手动输入分类导致匹配不到。

// 分类选择(下拉框)String?_selectedCategory;Widget_buildCategorySelect(){returnDropdownButtonFormField(value:_selectedCategory,hint:constText("请选择商品分类"),items:AppConstant.goodsCategory.skip(1)// 跳过“全部”,发布时不能选“全部”.map((category)=>DropdownMenuItem(value:category,child:Text(category),)).toList(),decoration:InputDecoration(border:OutlineInputBorder(borderRadius:BorderRadius.circular(8))),onChanged:(value)=>setState(()=>_selectedCategory=value),);}
  • 跳过“全部”分类(发布商品必须选具体分类,不能选“全部”);
  • 下拉选项直接复用全局常量,后期新增分类,这里自动同步,不用改代码;
  • 用DropdownButtonFormField,和其他输入框样式统一,视觉更整齐。

5. 成色选择(下拉选择,复用全局常量)

和分类选择逻辑一样,复用DAY2预置的商品成色常量,下拉选择,操作简单,贴合学生发布习惯。

// 成色选择(下拉框)String?_selectedCondition;Widget_buildConditionSelect(){returnDropdownButtonFormField(value:_selectedCondition,hint:constText("请选择商品成色"),items:AppConstant.goodsCondition.map((condition)=>DropdownMenuItem(value:condition,child:Text(condition),)).toList(),decoration:InputDecoration(border:OutlineInputBorder(borderRadius:BorderRadius.circular(8))),onChanged:(value)=>setState(()=>_selectedCondition=value),);}
  • 直接复用全局成色常量,不用重复写选项;
  • 样式和分类下拉框统一,保持页面整体风格一致;
  • 用String?类型,默认null,后续校验时判断是否选择。

6. 描述输入框(可选,可多行输入)

商品描述让用户补充商品细节(比如“无笔记、无划痕”),支持多行输入,限制最多200字,避免描述过长。

// 描述输入框finalTextEditingController_descCtrl=TextEditingController();Widget_buildDescInput(){returnTextField(controller:_descCtrl,maxLines:4,maxLength:200,decoration:InputDecoration(hintText:"请输入商品描述(可选,如:无划痕、无使用痕迹)",border:OutlineInputBorder(borderRadius:BorderRadius.circular(8)),),);}
  • maxLines设为4,支持多行输入,方便用户填写细节;
  • 限制200字,避免描述过于冗长;
  • 可选填,不用强制用户填写,更人性化。

7. 提交按钮(核心交互)

提交按钮要突出,用全局主色,点击后触发表单校验、数据封装、本地存储,最后返回首页并刷新列表。

// 提交按钮Widget_buildSubmitBtn(){returnSizedBox(width:double.infinity,child:ElevatedButton(style:ElevatedButton.styleFrom(backgroundColor:AppConstant.primary,padding:constEdgeInsets.symmetric(vertical:12),shape:RoundedRectangleBorder(borderRadius:BorderRadius.circular(8)),),onPressed:_submitGoods,// 点击触发提交逻辑child:constText("发布闲置",style:TextStyle(fontSize:16,color:Colors.white)),),);}
  • 按钮占满全屏宽度,视觉突出,方便点击;
  • 用全局主色,和AppBar颜色统一,风格一致;
  • 绑定_submitGoods方法,后续实现提交逻辑,代码解耦,方便维护。

三、实现表单校验+提交逻辑(本期重点)

提交按钮点击后,不能直接存数据,要先做校验(避免必填项为空、价格异常),校验通过后,再封装成GoodsModel对象,存入本地存储,最后返回首页并刷新列表,逻辑一步都不能少,新手跟着步骤来,不踩坑。

1. 表单校验逻辑(核心,避免无效提交)

先实现_submitGoods方法,第一步做校验,用大白话讲清楚校验规则:

  • 标题不能为空;
  • 价格不能为空、不能为0、不能为负数;
  • 分类必须选择;
  • 成色必须选择。
// 提交闲置逻辑void_submitGoods(){// 1. 获取所有输入内容Stringtitle=_titleCtrl.text.trim();StringpriceStr=_priceCtrl.text.trim();Stringdesc=_descCtrl.text.trim();// 2. 表单校验if(title.isEmpty){_showToast("请输入商品标题");return;}if(priceStr.isEmpty||double.parse(priceStr)<=0){_showToast("请输入有效的商品价格(大于0)");return;}if(_selectedCategory==null){_showToast("请选择商品分类");return;}if(_selectedCondition==null){_showToast("请选择商品成色");return;}// 校验通过,继续封装数据_saveGoods(title,priceStr,desc);}

2. 封装提示弹窗(复用,提升体验)

上面用到的_showToast方法,是提示用户“必填项未填写”“价格无效”的弹窗,单独封装,后续可复用,不用重复写代码。

// 提示弹窗(复用)void_showToast(Stringmsg){ScaffoldMessenger.of(context).showSnackBar(SnackBar(content:Text(msg),duration:constDuration(seconds:1),backgroundColor:Colors.grey.shade800,),);}
  • 用SnackBar做提示,贴合鸿蒙原生交互风格;
  • 提示时长1秒,不遮挡太久,用户体验友好;
  • 单独封装,后续其他页面也能复用。

3. 封装商品数据+存入本地存储

校验通过后,将所有输入内容封装成GoodsModel对象,生成唯一ID、获取当前时间,然后存入本地存储,最后返回首页并刷新列表。

// 封装商品数据,存入本地void_saveGoods(Stringtitle,StringpriceStr,Stringdesc){// 生成商品唯一ID(避免重复,用时间戳+随机数)StringgoodsId=DateTime.now().millisecondsSinceEpoch.toString();// 价格转成double类型double price=double.parse(priceStr);// 获取当前发布时间(格式:yyyy-MM-dd)Stringtime=DateFormat("yyyy-MM-dd").format(DateTime.now());// 封装成GoodsModel对象GoodsModelgoods=GoodsModel(id:goodsId,title:title,price:price,category:_selectedCategory!,condition:_selectedCondition!,desc:desc,time:time,);// 存入本地存储_saveToLocal(goods);}
  • 唯一ID:用当前时间戳(毫秒级),确保不会重复,新手不用搞复杂的ID生成方式,这个足够用;
  • 发布时间:自动获取当前日期,不用用户手动输入,更便捷;
  • 强制解包_selectedCategory和_selectedCondition(因为前面已经校验过,不会为null)。

4. 本地存储逻辑(对接DAY1的StorageUtil)

调用DAY1封装的StorageUtil工具类,将新发布的商品添加到本地列表中,然后返回首页,刷新首页列表,实现“发布即显示”。

// 存入本地存储,刷新首页void_saveToLocal(GoodsModelgoods){// 1. 获取本地已有的商品列表List<GoodsModel>localList=StorageUtil.getGoodsList();// 2. 添加新发布的商品到列表最前面(最新发布的置顶)localList.insert(0,goods);// 3. 重新保存到本地StorageUtil.saveGoodsList(localList);// 4. 返回首页,关闭发布页面Navigator.pop(context);// 5. 发送通知,让首页刷新列表(后续DAY5完善,今天先实现基础保存)_showToast("发布成功!");}
  • 最新发布的商品插入到列表最前面,实现“最新置顶”,符合用户习惯;
  • 保存完成后,关闭发布页面,返回首页,提示“发布成功”;
  • 首页刷新逻辑,我们DAY5完善(用通知或状态管理),今天先实现“发布后存入本地”,确保数据不会丢失。

四、补充:完善StorageUtil工具类(新增商品存储方法)

DAY1我们只写了基础的字符串存储,今天需要新增“商品列表”的存取方法,适配GoodsModel实体类,直接复制下面的代码,添加到util/storage_util.dart中。

// 新增:保存商品列表到本地staticFuture<void>saveGoodsList(List<GoodsModel>list)async{List<String>jsonList=list.map((e)=>jsonEncode(e.toJson())).toList();await_prefs.setStringList("goods_list",jsonList);}// 新增:从本地获取商品列表staticList<GoodsModel>getGoodsList(){List<String>?jsonList=_prefs.getStringList("goods_list");if(jsonList==null)return[];returnjsonList.map((e)=>GoodsModel.fromJson(jsonDecode(e))).toList();}
  • 和DAY2的商品实体类序列化对应,确保数据能正常存、正常读;
  • 存储key用“goods_list”,和之前的存储key区分开,避免冲突;
  • 空值兜底,第一次打开APP,本地没有商品列表时,返回空列表,不会报错。

五、UI细节优化+鸿蒙适配(毕设加分项)

  1. 统一样式:所有输入框、下拉框的圆角都是8,和卡片圆角呼应,视觉统一;
  2. 按钮状态:提交按钮可添加“加载中”状态(后期完善),避免用户重复点击;
  3. 输入提示:所有hintText都贴合校园场景,引导用户正确输入;
  4. 间距优化:所有表单间距统一,避免大小不一,视觉更整齐;
  5. 适配键盘:SingleChildScrollView确保键盘弹出时,表单不会被遮挡,鸿蒙手机适配无压力。

六、新手常见问题答疑(避坑重点)

问题1:提交后,本地存储存不上数据?

原因:1. 没完善StorageUtil的商品存取方法;2. 实体类序列化代码写错;3. 存储key和读取key不一致。
解决:直接复制本文中的StorageUtil新增代码,确保key是“goods_list”,序列化代码和实体类字段一致。

问题2:价格输入框能输入中文、符号?

原因:没设置keyboardType为numberWithOptions(decimal: true);
解决:检查_priceCtrl对应的TextField,加上keyboardType配置,重启运行即可。

问题3:提交后,首页列表不刷新?

原因:今天我们只实现了“存入本地”,还没做首页刷新逻辑,DAY5会完善(用通知刷新);
解决:暂时可以重启APP,就能看到新发布的商品,DAY5我们实现“发布后自动刷新”。

问题4:下拉选择分类/成色后,点击提交还是提示“请选择”?

原因:_selectedCategory或_selectedCondition没有用setState更新状态;
解决:检查DropdownButtonFormField的onChanged方法,确保用setState更新变量。

问题5:生成的商品ID重复?

原因:用了简单的随机数,容易重复;
解决:本文用“时间戳”生成ID,毫秒级时间戳不会重复,直接用本文的代码即可。


✅ DAY4 小结

今天我们完成了「发布闲置」核心功能,实打实落地了4件大事,难度适中,新手完全能跟上:

  1. 搭建了发布闲置完整页面布局,包含图片上传占位、所有表单组件,样式贴合鸿蒙风格;
  2. 实现了所有表单输入逻辑,绑定控制器,能正常获取输入内容;
  3. 做了表单校验,避免无效提交,提升用户体验;
  4. 对接本地存储,实现“发布商品存入本地”,返回首页提示发布成功。

现在,我们的APP已经能“发布闲置”了,虽然首页还不能自动刷新,但数据已经能正常存入本地,下一步就是完善首页刷新、我的发布页面,让整个流程闭环。

📅 DAY5 预告

DAY5 我们重点解决“发布后刷新”和“我的发布”页面,难度依旧循序渐进:

  1. 用通知机制,实现「发布闲置后,首页自动刷新列表」,不用重启APP;
  2. 搭建「我的发布」页面,展示当前用户发布的所有闲置商品;
  3. 实现“我的发布”页面与本地存储联动,实时同步发布、删除数据;
  4. 优化列表滑动流畅度,适配鸿蒙手机,避免卡顿;
  5. 补充“空数据占位”,我的发布页面没有商品时,显示空提示。

要不要我直接接着给你写DAY5完整正文,保持同风格、同结构、带精简代码、口语化讲解,直接可发CSDN?

http://www.jsqmd.com/news/786620/

相关文章:

  • OpenPawz/OPIDE:构建宠物健康数据开放生态的技术架构与实践
  • 混合信号神经形态芯片与脉冲神经网络在线学习算法
  • License Manager软件授权管理系统v1.1.2发布:新增配置模块,优化多项功能
  • OpenClaw热潮退去,用户吐槽部署繁琐、性价比低,Hermes成替代之选
  • RGBW LED矩阵调光技术与LT3965驱动方案详解
  • Zilliz Skill:构建标准化技能库,增强大语言模型工具调用能力
  • NiMH电池模拟锂电池的电源管理方案设计与实现
  • 那个从不加班的同事,晋升却比我快,我偷学了他的工作流
  • 2026年4月岗亭出售厂家推荐,岗亭售货亭/岗亭移动厕所/移动岗亭/停车场岗亭/成品移动岗亭,岗亭实力厂家口碑推荐 - 品牌推荐师
  • AI创作全链路实战:从代码生成到视觉海报批量制作完整指南
  • CANN/pypto argsort排序索引
  • ChatLLM.cpp:纯C++本地大模型推理引擎部署与实战指南
  • 毕业两年了,25岁转行网络安全来得及吗?网络运维安全培训+就业(职等你来)
  • Modern-Cursors-v2:现代化鼠标光标主题的设计、安装与深度定制指南
  • Tracciatto:为现代Ruby项目设计的VS Code深度调试扩展
  • 哪里可以找到 Linux 简介教程?
  • 抽蓄电站加劲环压力明管结构可靠性智能优化【附模型】
  • ComfyUI-Bridge:AI绘画工作流转换工具,实现SD WebUI到ComfyUI的无缝迁移
  • 基于Zilliz-Skill框架构建AI智能体技能:从原理到工程实践
  • FastbootEnhance:Windows上最直观的Fastboot工具箱,告别命令行恐惧症
  • claud code 学习记录
  • CoolRunner-II CPLD低功耗设计与DataGATE技术解析
  • 2026届学术党必备的六大AI写作助手推荐榜单
  • ARM与Thumb指令集架构解析及优化实践
  • 告别“凭感觉编程”:AI应用开发的工程化避坑指南与OpenSpec实践
  • 技术分享的内卷化:从知识传播到表演竞赛的异化
  • 从零构建文档问答技能:RAG架构、LangChain实践与OpenClaw集成
  • 信息安全工程师-病毒、木马、蠕虫技术原理与防御基础
  • cann/ops-nn ELU梯度V2算子
  • VSCode光标增强插件开发:从CSS注入到动态效果实现