Flutter for OpenHarmony 校园闲置跳蚤市场APP 实战DAY4发布闲置页面表单校验本地存储提交欢迎加入开源鸿蒙跨平台社区https://openharmonycrossplatform.csdn.net哈喽各位小伙伴咱们校园闲置跳蚤市场连载来到DAY4啦 进度稳步推进新手跟着敲完全能跟上全程不跳步、不搞复杂概念还是老规矩口语化大段讲解每处关键功能附5–6行精简代码不堆冗余代码写完直接能发CSDN兼顾实用性和毕设规范今天重点搞定「发布闲置」这个核心功能——毕竟有发布才能有真正的闲置商品这也是整个APP的核心交互之一回顾前三期进度帮大家快速回顾避免脱节DAY1新建项目、配依赖、搭底部Tab导航、全局初始化打好项目地基DAY2写商品实体类、预置校园分类/成色常量、搭首页顶部分类标签DAY3封装商品卡片、造模拟假数据、实现分类联动筛选首页完全成型。今天DAY4我们聚焦「发布闲置」页面从布局搭建、表单输入、校验到提交保存、同步刷新首页一步步落地难度循序渐进新手不用慌每一步都有详细讲解和精简代码照抄就能跑通。 DAY4 本期开发目标详细版新手必看搭建「发布闲置」完整页面布局贴合校园场景包含多图上传占位、标题输入、价格输入、分类选择、成色选择、描述输入、提交按钮实现所有表单输入逻辑绑定控制器确保输入内容能正常获取做表单校验必填项不能为空、价格不能为0/负数避免无效提交提升用户体验对接本地存储实现「发布闲置提交后自动存入本地首页实时刷新」生成商品唯一ID避免重复、自动获取当前发布时间不用手动输入优化发布页面UI贴合鸿蒙简约风格和首页、卡片样式统一细节拉满解决新手常见的「发布后不刷新」「数据存不上」「表单报错」等问题提前避坑。一、搭建发布闲置页面整体布局核心UI发布页面是用户交互的关键布局要清晰、操作要简单贴合学生使用习惯——不用复杂排版按「图片上传→基本信息→提交」的顺序排布一目了然。先打开page/publish/publish_page.dartDAY1已经建好文件夹直接新建这个文件整体布局用SingleChildScrollView包裹避免小屏手机键盘弹出后遮挡表单这是新手最容易忽略的点加上这个体验更友好。整体布局核心代码精简版直接复制importpackage:flutter/material.dart;importpackage:campus_flea_market/config/app_color.dart;importpackage:campus_flea_market/config/app_constant.dart;classPublishPageextendsStatefulWidget{constPublishPage({super.key});overrideStatePublishPagecreateState()_PublishPageState();}class_PublishPageStateextendsStatePublishPage{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_titleCtrlTextEditingController();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_priceCtrlTextEditingController();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(()_selectedCategoryvalue),);}跳过“全部”分类发布商品必须选具体分类不能选“全部”下拉选项直接复用全局常量后期新增分类这里自动同步不用改代码用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(()_selectedConditionvalue),);}直接复用全局成色常量不用重复写选项样式和分类下拉框统一保持页面整体风格一致用String?类型默认null后续校验时判断是否选择。6. 描述输入框可选可多行输入商品描述让用户补充商品细节比如“无笔记、无划痕”支持多行输入限制最多200字避免描述过长。// 描述输入框finalTextEditingController_descCtrlTextEditingController();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(_selectedCategorynull){_showToast(请选择商品分类);return;}if(_selectedConditionnull){_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避免重复用时间戳随机数StringgoodsIdDateTime.now().millisecondsSinceEpoch.toString();// 价格转成double类型double pricedouble.parse(priceStr);// 获取当前发布时间格式yyyy-MM-ddStringtimeDateFormat(yyyy-MM-dd).format(DateTime.now());// 封装成GoodsModel对象GoodsModelgoodsGoodsModel(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. 获取本地已有的商品列表ListGoodsModellocalListStorageUtil.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中。// 新增保存商品列表到本地staticFuturevoidsaveGoodsList(ListGoodsModellist)async{ListStringjsonListlist.map((e)jsonEncode(e.toJson())).toList();await_prefs.setStringList(goods_list,jsonList);}// 新增从本地获取商品列表staticListGoodsModelgetGoodsList(){ListString?jsonList_prefs.getStringList(goods_list);if(jsonListnull)return[];returnjsonList.map((e)GoodsModel.fromJson(jsonDecode(e))).toList();}和DAY2的商品实体类序列化对应确保数据能正常存、正常读存储key用“goods_list”和之前的存储key区分开避免冲突空值兜底第一次打开APP本地没有商品列表时返回空列表不会报错。五、UI细节优化鸿蒙适配毕设加分项统一样式所有输入框、下拉框的圆角都是8和卡片圆角呼应视觉统一按钮状态提交按钮可添加“加载中”状态后期完善避免用户重复点击输入提示所有hintText都贴合校园场景引导用户正确输入间距优化所有表单间距统一避免大小不一视觉更整齐适配键盘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件大事难度适中新手完全能跟上搭建了发布闲置完整页面布局包含图片上传占位、所有表单组件样式贴合鸿蒙风格实现了所有表单输入逻辑绑定控制器能正常获取输入内容做了表单校验避免无效提交提升用户体验对接本地存储实现“发布商品存入本地”返回首页提示发布成功。现在我们的APP已经能“发布闲置”了虽然首页还不能自动刷新但数据已经能正常存入本地下一步就是完善首页刷新、我的发布页面让整个流程闭环。 DAY5 预告DAY5 我们重点解决“发布后刷新”和“我的发布”页面难度依旧循序渐进用通知机制实现「发布闲置后首页自动刷新列表」不用重启APP搭建「我的发布」页面展示当前用户发布的所有闲置商品实现“我的发布”页面与本地存储联动实时同步发布、删除数据优化列表滑动流畅度适配鸿蒙手机避免卡顿补充“空数据占位”我的发布页面没有商品时显示空提示。要不要我直接接着给你写DAY5完整正文保持同风格、同结构、带精简代码、口语化讲解直接可发CSDN