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

ArkTS 状态机实战:中式美食列表页怎么区分加载中、空结果和错误态

中式美食的列表页如果只写recipes.length === 0就显示空态,早晚会出问题。第一次进入页面时数组是空的,搜索没有结果时数组也是空的,数据库读取失败时数组还是空的。用户看到同一个空页面,根本分不清是正在加载、真的没菜谱,还是系统出错了。

我现在更倾向于让 ViewModel 明确告诉页面当前是什么状态。页面不要猜,也不要把业务判断写在build()里。加载中、空结果、错误态、正常列表、刷新中都应该是不同状态,用户看到的反馈也应该不一样。

环境和边界

项目说明
应用中式美食
技术栈HarmonyOS / ArkTS / ArkUI
主要页面首页菜谱列表、分类列表、搜索结果列表
主要模块RecipeListPageRecipeListViewModelRecipeRepositoryRecipeListState
核心目标页面不靠数组长度猜状态,而是消费明确状态

这篇只处理列表状态,不展开讲分页加载、复杂缓存和推荐排序。先把状态边界稳住,后面加筛选、分页、收藏同步时才不容易乱。

为什么数组长度不够用

场景数组长度用户应该看到
首次进入列表0加载中或骨架屏
搜索没有结果0“没有找到相关菜谱”,带清空搜索入口
分类下面暂无菜谱0分类空态,引导换分类
数据库读取失败0错误提示和重试按钮
正常列表大于 0菜谱卡片列表

如果这些场景都用一个空态处理,用户会误解。比如数据库失败时显示“暂无菜谱”,用户会以为应用里真的没有内容;首次加载慢一点时显示空态,用户会觉得数据丢了。

状态模型先写清楚

我会把列表状态写成一个联合类型。这样页面不会同时出现loading=trueerror=truerecipes=[]这种互相打架的情况。

typeRecipeListState=|{type:'initialLoading'}|{type:'refreshing';recipes:RecipeCardModel[]}|{type:'empty';reason:EmptyReason;keyword?:string;categoryName?:string}|{type:'error';message:string;retryable:boolean}|{type:'ready';recipes:RecipeCardModel[]}typeEmptyReason='search'|'category'|'favorite'|'all'

refreshing里保留旧列表,是为了刷新时不让页面突然白掉。用户已经看到一批菜谱了,刷新过程中保留旧内容,再给一个轻提示,比整页切成 loading 更舒服。

Repository 只返回数据结果

Repository 不需要知道页面怎么展示,它只负责把数据读出来。错误就抛错,空结果就返回空数组,不要在 Repository 里拼 UI 文案。

interfaceRecipeQuery{keyword?:stringcategoryId?:stringfavoriteOnly?:boolean}interfaceRecipeQueryResult{items:RecipeCardModel[]query:RecipeQuery}classRecipeRepository{asyncqueryRecipes(query:RecipeQuery):Promise<RecipeQueryResult>{constitems=awaitrecipeDao.queryCards({keyword:query.keyword,categoryId:query.categoryId,favoriteOnly:query.favoriteOnly??false})return{items,query}}}

Repository 的边界越干净,ViewModel 越好写。不要让数据层判断“这是搜索空态还是分类空态”,因为这属于页面语义。

ViewModel 负责翻译成页面状态

ViewModel 拿到 Repository 结果以后,再决定页面应该显示什么。这里才知道用户当前是搜索、分类、收藏还是全部列表。

classRecipeListViewModel{privatestate:RecipeListState={type:'initialLoading'}constructor(privaterecipeRepository:RecipeRepository){}asyncload(query:RecipeQuery):Promise<void>{this.state={type:'initialLoading'}try{constresult=awaitthis.recipeRepository.queryRecipes(query)this.state=this.toState(result)}catch(error){this.state={type:'error',message:'菜谱列表加载失败,可以稍后再试',retryable:true}}}asyncrefresh(query:RecipeQuery):Promise<void>{constoldRecipes=this.state.type==='ready'?this.state.recipes:[]this.state={type:'refreshing',recipes:oldRecipes}try{constresult=awaitthis.recipeRepository.queryRecipes(query)this.state=this.toState(result)}catch(error){this.state=oldRecipes.length>0?{type:'ready',recipes:oldRecipes}:{type:'error',message:'刷新失败,请重试',retryable:true}}}getState():RecipeListState{returnthis.state}}

刷新失败时我不一定切到整页错误态。如果旧列表还在,就保留旧列表,给一个轻提示会更合适。整页错误态更适合首次加载失败。

空态原因要分开

空结果也不是一种。搜索无结果、分类暂无菜谱、收藏为空,文案和按钮都不一样。

privatetoState(result:RecipeQueryResult):RecipeListState{if(result.items.length>0){return{type:'ready',recipes:result.items}}if(result.query.keyword){return{type:'empty',reason:'search',keyword:result.query.keyword}}if(result.query.categoryId){return{type:'empty',reason:'category',categoryName:this.findCategoryName(result.query.categoryId)}}if(result.query.favoriteOnly){return{type:'empty',reason:'favorite'}}return{type:'empty',reason:'all'}}

这里的重点不是代码多,而是让空态能说人话。用户搜“红烧”没结果,应该提示换关键词;收藏为空,应该引导去浏览和收藏菜谱。

ArkUI 页面只按状态渲染

页面层不要再判断recipes.length。它只根据state.type渲染对应组件。

@Componentstruct RecipeListPage{@StateprivatelistState:RecipeListState={type:'initialLoading'}privateviewModel=newRecipeListViewModel(recipeRepository)privatecurrentQuery:RecipeQuery={}aboutToAppear():void{this.viewModel.load(this.currentQuery).then(()=>{this.listState=this.viewModel.getState()})}build(){Column(){if(this.listState.type==='initialLoading'){RecipeListSkeleton()}elseif(this.listState.type==='error'){ErrorStateView({message:this.listState.message,showRetry:this.listState.retryable,onRetry:()=>this.reload()})}elseif(this.listState.type==='empty'){RecipeEmptyView({reason:this.listState.reason,keyword:this.listState.keyword,categoryName:this.listState.categoryName,onClear:()=>this.clearFilter()})}else{RecipeCardList({recipes:this.listState.recipes})}}}privatereload():void{this.viewModel.load(this.currentQuery).then(()=>{this.listState=this.viewModel.getState()})}}

这段页面代码会比一堆布尔变量更好维护。以后加分页、筛选、推荐入口,仍然是先改状态,再让页面渲染状态。

空态文案也要跟场景走

空态原因文案方向推荐动作
搜索无结果没找到包含这个关键词的菜谱清空搜索、换关键词
分类暂无当前分类还没有菜谱切换分类、查看全部
收藏为空还没有收藏菜谱去首页浏览
全部为空本地还没有菜谱数据导入或刷新数据

空态文案不要统一写“暂无数据”。这句话对开发者省事,对用户没有帮助。

验收时我会这样测

场景操作期望结果
首次进入打开首页列表先显示骨架屏,再显示菜谱
搜索无结果搜一个不存在的词显示搜索空态和清空入口
分类为空切到没有菜谱的分类显示分类空态,不说系统错误
Repository 抛错模拟数据库读取失败显示错误态和重试按钮
重试成功错误态点击重试恢复正常列表
刷新失败有旧列表时刷新失败保留旧列表,不整页变空

这里我特别看重“刷新失败”。很多应用第一次加载没问题,但刷新失败时会把旧内容清掉,用户就以为内容没了。对中式美食这种列表浏览应用来说,旧内容能保留就尽量保留。

失败用例提前兜住

失败用例可能后果处理方式
把 loading 和 empty 混用首屏闪一下“暂无数据”initialLoading单独表示
搜索失败显示空态用户误以为真没结果Repository 抛错时进入error
刷新失败清空旧列表用户以为数据丢失refreshing保留旧列表
空态文案统一用户不知道下一步做什么EmptyReason给动作
页面直接改数组ViewModel 状态失控页面只触发动作,不直接拼状态

这些失败用例不是为了把代码写复杂,而是为了避免用户看到错误反馈。状态拆清楚以后,页面和数据层都会少一点猜测。

这次拆完后的判断

列表页状态机不是为了“架构好看”,而是为了让用户知道当前到底发生了什么。中式美食后面会继续加搜索、分类、收藏、推荐和分页,如果一开始只靠数组长度判断,后面每个入口都会出现奇怪空态。

我现在的做法是:Repository 只返回数据,ViewModel 把数据结果翻译成页面状态,ArkUI 页面只按状态渲染。这个边界一旦稳住,后面加筛选和推荐时,页面不会变成一堆猜测条件。

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

相关文章:

  • 如何让AI成为团队最靠谱的“超级同事”
  • 韩国主权AI战略:集中万块GPU加速自主大模型研发
  • ChatGPT API实战指南:从GPT-3.5到GPT-4o的集成、调优与深度排错
  • AI 大模型智能体应用软件定制开发公司——天磊卫士(深圳)科技有限公司
  • 3分钟学会Figma设计稿一键转代码:告别手动切图的终极指南
  • 膜结构汽车棚厂家哪家报价透明?
  • CCS + J-Link 调试立创地猛星 MSPM0G3507 无法烧录的一次恢复记录:“There is already a Code Composer debug session running“
  • MT5 科普:Python 如何辅助金融时间序列分析
  • 性价比高的书房书柜整木高定排行
  • Polar Code 编解码 MATLAB 实现
  • 第八次作业-用户管理和文件权限
  • PowerToys:Windows生产力的终极革命
  • 网盘直链下载助手:告别限速!九大网盘高速下载终极指南
  • AI编程实战指南:从工具选型到高效协作的完整工作流
  • 打造专属B站体验:Bilibili-Evolved模块化增强指南
  • 苹果电脑硬盘扩容到底有没有风险?一个华强北硬件工程师的实话
  • 县域餐饮差异化经营方法论:武当山村宴实战案例助力门店获客
  • 可灵AI NEXTGEN:突破视频生成技术瓶颈,实现人物一致性与场景连贯性
  • ArcGIS Pro镶嵌数据集:绝不仅是数据之和
  • 工业级信号隔离与抗干扰设计实战解析
  • 3步解锁AcFun视频离线保存:开源工具AcFunDown实用指南
  • SLAM 将PCL点云转换成网格地图
  • 终极指南:5分钟掌握Figma设计稿到多平台代码的智能转换
  • D3KeyHelper:暗黑破坏神3终极自动化工具,5分钟打造你的专属游戏助手
  • 虚拟文化体验翻译:虚拟导览系统的多语言交互与信息架构设计
  • 网盘直链下载助手完整教程:三步实现满速下载,告别客户端依赖
  • XUnity.AutoTranslator翻译失效:5步快速诊断与终极修复方案
  • LinkSwift网盘直链下载助手:告别限速!9大网盘高速下载完整解决方案
  • 洛雪音乐桌面版:打破平台壁垒的终极音乐聚合解决方案
  • 直播/录屏必看!采集卡和显卡到底有啥区别?搭配用法一次讲透✅