
中式美食的菜谱列表页一开始数据少页面状态怎么写都能看起来正常。等搜索、分类、分页、网络失败、本地空数据都进来以后问题就出来了正在加载时显示“暂无数据”请求失败后旧列表还在下面刷新时页面闪白。这个时候不要先改样式先把页面状态拆清楚。先说清楚有哪些状态一个列表页至少有四种主状态加载中、空结果、错误、正常内容。它们不能同时抢主画面。状态什么时候出现不能和谁同时出现loading首次加载或重新搜索empty、errorempty请求成功但没有数据loading、errorerror请求失败且没有可用数据loading、emptycontent有列表数据首屏 loading为什么要这么拆原因是用户只关心当前页面到底发生了什么。如果页面同时告诉他“正在加载”和“暂无数据”他会以为搜索没结果如果错误提示盖着旧内容他又不知道旧内容还能不能点。坏写法多个布尔值互相打架很多页面一开始会这样写Stateloading:booleanfalseStateempty:booleanfalseStateerror:booleanfalseStaterecipes:RecipeCardVM[][]问题是这几个布尔值可能被不同分支修改最后出现矛盾。asyncloadRecipes(keyword:string){this.loadingtruetry{this.recipesawaitRecipeRepository.instance.search(keyword)this.emptythis.recipes.length0}catch(err){this.errortrue}this.loadingfalse}这段代码少了几件事请求成功时没有清掉 error请求失败时没有处理 empty下一次搜索开始时也没有把旧状态重置。结果页面很容易一边有数据、一边还挂着错误。好写法用一个 pageState 收口我更愿意用一个主状态字段把页面当前状态收口。typePageStateloading|empty|error|contentStatepageState:PageStateloadingStaterecipes:RecipeCardVM[][]StateerrorText:string加载时只改这个状态asyncloadRecipes(keyword:string){this.pageStateloadingthis.errorTexttry{constnextawaitRecipeRepository.instance.search(keyword)this.recipesnextthis.pageStatenext.length0?content:empty}catch(err){this.recipes[]this.errorText菜谱加载失败稍后再试this.pageStateerror}}这样写的好处是清楚。页面任意时刻只有一种主状态不会同时 empty 又 error也不会 loading 结束后忘了清掉旧错误。ArkUI 里怎么展示页面构建时也按主状态分支不要在一堆if里反复插组件。build(){Column(){SearchBar({value:this.keyword,onSearch:(value:string)this.loadRecipes(value)})if(this.pageStateloading){LoadingPanel()}elseif(this.pageStateempty){EmptyPanel({keyword:this.keyword})}elseif(this.pageStateerror){ErrorPanel({message:this.errorText,onRetry:()this.loadRecipes(this.keyword)})}else{RecipeList({recipes:this.recipes})}}}这段代码看起来普通但实际能解决很多乱象。加载、空态、错误、正文的位置固定状态变化时不会互相叠。写法页面表现后期维护多个布尔值分散控制容易互相叠每加一个状态都要到处改一个 pageState 收口主画面稳定状态入口清楚数据和状态混着判断边界模糊容易误判空态状态不要从组件里面反推还有一个坑子组件里自己判断空态。比如RecipeList里面发现recipes.length 0就自己显示暂无数据。外层页面又根据pageState显示EmptyPanel最后空态可能出现两次。更稳的做法是父页面决定主状态子组件只展示自己该展示的内容。Componentstruct RecipeList{Proprecipes:RecipeCardVM[]build(){List(){ForEach(this.recipes,(item:RecipeCardVM){ListItem(){RecipeCard({recipe:item})}},(item:RecipeCardVM)item.id)}}}RecipeList不处理 empty也不处理 error。它只负责“有数据时怎么排列表”。这样组件职责清楚后面你要换空态图、错误文案、重试按钮都在页面层改不会藏在列表组件里面。组件负责什么不负责什么页面容器判断 pageState具体卡片内部样式RecipeList渲染列表空态、错误态EmptyPanel展示空态和引导重新查数据ErrorPanel展示错误和重试按钮直接改 recipes这种拆法看起来多了几个小组件实际是在避免状态分散。页面状态一旦散到多个组件里后期加分页、筛选、重试时就很难判断到底是谁把页面改成了空态。搜索和分类要带上请求版本如果页面有搜索输入还要注意请求返回顺序。用户连续输入“红”“红烧”“红烧肉”可能后发的请求先回来先发的请求后回来。这个时候如果没有请求版本旧结果可能覆盖新结果。privaterequestVersion:number0asyncsearch(keyword:string){constversionthis.requestVersionthis.pageStateloadingtry{constnextawaitRecipeRepository.instance.search(keyword)if(version!this.requestVersion){return}this.recipesnextthis.pageStatenext.length0?content:empty}catch(err){if(version!this.requestVersion){return}this.errorText搜索失败稍后再试this.pageStateerror}}这段代码解决的是另一个常见错觉页面不是状态判断错了而是旧请求回来得太晚把新状态盖掉了。搜索、分类、排序同时存在时这个问题更明显。刷新时不要闪白有一种细节要分开首次加载和已有内容刷新。首次进入页面没有数据可以显示 loading但用户已经看到列表后再下拉刷新或切换排序不一定要把整页切到 loading否则页面会闪白。StatepageState:PageStateloadingStaterefreshing:booleanfalseasyncrefreshRecipes(){if(this.recipes.length0){this.pageStateloading}else{this.refreshingtrue}try{constnextawaitRecipeRepository.instance.search(this.keyword)this.recipesnextthis.pageStatenext.length0?content:empty}finally{this.refreshingfalse}}这里的判断标准是没有旧内容时用主 loading有旧内容时用局部 refreshing。这样用户不会每次筛选都看到整页消失。怎么验证这个页面状态问题要用不同路径测。验证动作期待结果能发现什么首次打开有数据先 loading再 content首屏状态是否正确搜索不存在的菜显示 empty空态是否误叠 loading模拟查询失败显示 error错误是否清掉旧空态有列表时刷新列表不闪白refreshing 是否和 loading 分开错误后重试成功回到 contenterror 是否被清掉我会特别看“错误后重试成功”。如果重试成功了但错误提示还在说明状态没有收口。以后怎么避免以后写 ArkUI 页面时先别急着堆组件。先把页面主状态列出来再决定每个状态对应什么 UI。数据为空不等于页面空态请求中也不等于没有数据。中式美食里搜索页、分类页、收藏页、购物清单页都可以用这套思路。一个pageState管主画面一个refreshing管局部刷新一个errorText管错误文案。边界清楚后页面不会因为多一个筛选条件就开始乱跳也不会让用户看见“正在加载”和“暂无数据”同时出现。