Harmony os 技术实战|拼豆制图01:用状态机收口 ArkUI 单页导航
拼豆制图这种工具型应用,入口看起来只有几个 Tab,但真正麻烦的是入口之间会共享图纸、搜索词、收藏状态和生成结果。若每个页面都自己维护一套跳转和数据,后期加详情页、放大页、导出页时很容易出现“返回到了错误页面”或者“收藏状态没同步”的问题。
这篇文章会把问题拆到能落地的层面,重点解决:
- 用一个入口状态承接多页面切换,而不是让页面彼此直接互相控制。
- 让图纸详情、放大查看和底部导航之间的关系更清晰。
- 把 UI 编排和图片转换、导出等业务服务拆开。
- 为后续拆成多 Page 或 Navigation 路由保留迁移空间。
为什么导航先要收口
在当前项目中,Index.ets承担了入口页职责:用户可以从首页看热门图纸,从图库筛选 50 张内置图纸,从制作页上传图片生成编号图,也可以进入收藏和个人页。真正的风险不是页面多,而是这些页面都会碰到同一个图纸对象。例如首页卡片点击、图库卡片点击、收藏列表点击,最终都应该进入编号图纸页,并且读到同一个selectedPatternId。
如果把每个入口都写成独立跳转,短期能跑,长期会出现两类问题:一类是页面状态散在多个地方,另一类是返回路径无法推断。工具型应用的交互频率高,用户会反复在“找图纸、看图纸、做图纸、保存图纸”之间切换,因此入口状态要像一个小型状态机一样可读、可改、可验证。
落地时可以先抓三个信号:
ArkUI的职责是否只在一个层级里被定义,而不是页面、服务和资源各写一份。activeTab相关状态是否能从用户入口一路追到结果页,中间没有隐式副作用。- 遇到空数据、取消操作或边界输入时,页面是否还能停在可继续操作的状态。
页面边界不要和业务逻辑混在一起
单页导航并不是把所有代码塞进一个文件,而是在当前阶段先把会话状态统一放在顶层。页面 Builder 负责展示和触发事件,业务服务负责转换、导出、查询。这样做的好处是:当项目还在快速迭代 UI 时,入口关系稳定;当功能变重后,也可以把某个 Builder 提升成独立页面或组件。
| 决策点 | 推荐做法 | 避免的问题 |
|---|---|---|
| 入口状态 | activeTab保存当前主入口 | Tab 图标变化了但内容没变 |
| 图纸状态 | selectedPatternId指向当前图纸 | 详情页拿到过期对象 |
| 底部导航 | 只在非放大页显示 | 放大查看时底部栏遮挡图纸 |
| 业务能力 | 转换和导出放在 Service | 页面 Builder 写入大量异步逻辑 |
这张表真正约束的是变更顺序:先固定“入口状态”的归属,再处理“图纸状态”的输入输出,最后围绕“底部导航”做回归。只要这三处没有漂移,后续增加页面、资源或参数时,改动就不会一路扩散到无关模块。实际排查时也建议按这个顺序记录结论:哪一层接收输入、哪一层生成结果、哪一层负责失败后的恢复。记录得越具体,下一轮迭代越不容易把已经稳定的路径改坏。
先定义导航和图纸状态
导航状态不需要一开始就设计得很复杂,但命名必须稳定。TabItem是底部导航的数据形态,Pattern是页面之间共享的图纸对象;这两个模型只描述数据,不夹带页面行为。
1. 用稳定模型描述导航入口
底部导航不直接写死在 UI 里,先收成一个小模型,后续增加入口或替换图标时改动面会小很多。
exportinterfaceTabItem{key:'home'|'gallery'|'create'|'favorite'|'profile';label:string;icon:string;}exportinterfacePatternRouteState{activeTab:TabItem['key']|'numbered';selectedPatternId:string;isChartExpanded:boolean;}这个边界只负责说明“现在在哪”和“正在看哪张图纸”。它不关心图纸如何生成,也不关心卡片如何渲染,因此可以被首页、图库和收藏列表共同复用。
build 分支只做视图选择
页面分支应该像目录一样清楚。build中只根据activeTab决定显示哪个视图,复杂计算放到 private 方法,避免在 Builder 内写临时变量和业务判断。
2. build 分支保持声明式
顶层build的职责是选择页面,别把筛选、导出、图片解析等逻辑塞进来。
build(){Column(){if(this.activeTab==='numbered'&&this.isChartExpanded){this.ExpandedChartPage(this.getSelectedPattern());}elseif(this.activeTab==='home'){this.HomePage();}elseif(this.activeTab==='gallery'){this.GalleryPage();}elseif(this.activeTab==='create'){this.CreatePage();}elseif(this.activeTab==='numbered'){this.NumberedPage();}elseif(this.activeTab==='favorite'){this.FavoritePage();}else{this.ProfilePage();}if(!this.isChartExpanded){this.BottomNavigation();}}}这段代码的意图是让视图关系一眼可见。放大页优先判断,是因为它会改变底部导航可见性;普通主入口按稳定顺序排列,后续排查时能快速定位分支。
点击入口要同时更新业务上下文
用户点击图纸卡片时,不只是在切换页面,还在更新当前图纸上下文。把这两个动作放在同一个事件里,可以避免详情页依赖上一轮的图纸状态。
3. 图纸点击同时更新上下文和入口
从首页、图库、收藏进入详情时,事件处理要写成同一套动作。
privateopenPattern(patternId:string):void{this.selectedPatternId=patternId;this.isChartExpanded=false;this.activeTab='numbered';this.exportStatus='';}selectedPatternId是业务上下文,activeTab是页面上下文,二者必须一起更新。顺手清空导出状态,是为了避免上一张图纸的保存提示留到下一张图纸上。
返回和放大页要有明确出口
放大编号图是一个特殊状态:它仍然属于编号图纸能力,但需要隐藏底部导航并保持返回路径。这里不需要新增一个主 Tab,用布尔状态表达更直接。
4. 给放大页一个独立出口
放大查看不是新的主入口,它只是编号图纸页的展开状态。
privateshowExpandedChart():void{this.isChartExpanded=true;}privatecloseExpandedChart():void{this.isChartExpanded=false;this.activeTab='numbered';}这个写法避免了返回时猜测来源页面。无论用户从首页还是收藏进入图纸,一旦关闭放大图,都回到编号图纸视图,页面关系更可预测。
模块配置给入口一个稳定身份
入口模块本身也要在module.json5里有稳定身份。应用的主 Ability、图标、启动页和页面 profile 都应该指向明确资源,否则调试时容易把 UI 问题误判成页面逻辑问题。
5. 入口 Ability 配置保持单一
配置层只暴露一个主入口,业务页面由 ArkUI 状态控制。
{ "module": { "name": "entry", "type": "entry", "pages": "$profile:main_pages", "abilities": [{ "name": "EntryAbility", "srcEntry": "./ets/entryability/EntryAbility.ets", "exported": true }] } }Stage 模型下,入口 Ability 负责启动应用,应用内部的首页、图库、制作页不必都暴露成系统入口。这样能降低配置噪声,也方便后续统一处理启动参数。
验证这条导航链路
实际项目里,验证不能只看页面有没有打开。更稳的做法是把入口、状态、边界数据和失败路径都走一遍,尤其是拼豆图纸这种“看起来能显示,放大后才暴露问题”的功能。
验证前建议准备一组固定样本:一条正常路径、一条空数据路径、一条失败路径,再加一条连续操作路径。这样每次改动都能比较同一批场景,不会只凭当前页面肉眼感觉判断。如果涉及屏幕尺寸、资源替换或系统能力,还要保留修改前后的截图和关键输入,方便回退时确认差异来自哪里。
hvigor assembleHap--no-daemonWrite-Host"真机或模拟器验证:首页 -> 图库 -> 编号图纸 -> 放大图 -> 返回 -> 收藏"- 从首页热门图纸进入编号图纸页,确认标题、色号、图例属于同一张图纸。
- 从图库筛选后进入编号图纸页,再返回首页,确认底部导航没有停在错误状态。
- 打开放大图后确认底部导航隐藏,关闭后仍回到编号图纸页。
- 连续点击两张不同图纸,确认导出状态不会串到下一张图纸。
- 把
activeTab改成未知值时,要有兜底页面或回到首页。
常见问题和处理
| 现象 | 先看哪里 | 处理方式 |
|---|---|---|
| Tab 高亮变了但内容没变 | 是否只更新了图标状态 | 把内容分支统一绑定到activeTab |
| 进入详情后仍显示上一张图 | selectedPatternId更新顺序 | 点击事件里先写图纸 ID 再切换页面 |
| 放大页底部被遮住 | isChartExpanded判断位置 | 放大状态下不渲染底部导航 |
| 返回路径混乱 | 是否存在多个返回入口 | 集中到closeExpandedChart或activeTab='home' |
后续可以怎样演进
当页面继续增多时,可以把activeTab迁移成更正式的路由枚举,并把openPattern、closeExpandedChart这类动作收进一个轻量的导航服务。若后续需要从系统分享、通知或桌面快捷方式进入某张图纸,再在 Ability 启动参数里解析目标图纸 ID,然后写入同一套页面状态。
继续推进时建议保持三条约束:
- 先让现有主路径可回归,再拆更细的组件或服务。
- 新增状态必须能说明来源、更新时机和失败后的保留策略。
- 新增资源或配置要能从页面反查到生成来源,避免后期只靠人工记忆维护。
小结
单页导航的关键不是“少建页面”,而是先把入口状态和业务上下文放在一个可推断的位置。拼豆制图当前阶段用activeTab + selectedPatternId + isChartExpanded就能覆盖主要路径;页面负责展示,Service 负责业务,后续拆分也不会伤到用户路径。