HarmonyOS运动健康——架构总览与通用模式总结 哪些结构在每个页面都出现哪些设计决策贯穿始终哪些坑踩了不止一次把8个页面放在一起比较会发现它们的骨架惊人地一致。完整效果一、页面骨架Stack Scroll BottomNav所有页面的build方法都遵循同一个结构Stack({ alignContent: Alignment.Bottom }) { Scroll() { Column() { // 内容区 } } // 底部导航栏仅首页或 空白占位其他页面 }首页用Stack包裹底部放导航栏NavBtn。其他页面不放导航栏但保留了Stack结构——Scroll在外面是固定的内容在Scroll里面可滚动。Blank().height(80)在Scroll末尾留出底部空间防止内容被导航栏遮挡虽然非首页没有导航栏但留白保持了统一的滚动体验。这种固定头可滚动内容的模式是移动端的标准布局——用户滑动时导航栏不动内容区滚动。ArkUI中用Scroll组件实现比iOS的UIScrollView或Android的RecyclerView更简洁。二、导航栏的四种右侧按钮每个页面的导航栏左侧都是返回按钮SymbolGlyph chevron_left但右侧各不相同页面右侧按钮功能是否实现Index渐变头像Stack用户头像装饰WorkoutPage无——ExerciseDetail❤️ 收藏切换收藏状态已实现ProgressPage无——MealPage 添加添加饮食记录未实现CommunityPage发布发布新帖子已实现UserProfile关注按钮切换关注状态已实现ProfilePage⚙️ 设置进入设置未实现导航栏右侧是全局操作区——每个页面可以放不同的按钮。已实现的三个交互收藏、发布、关注都用了State变量控制状态切换。未实现的三个头像、、⚙️都是纯装饰。三、颜色常量的语义化所有页面用同一套颜色常量const A: string #FF4757 // 主色红色运动 const BG: string #F8F7FC // 背景浅灰紫 const CW: string #FFFFFF // 卡片白色 const T1: string #1E1B2E // 主文字深色 const T2: string #888888 // 次文字灰色 const T3: string #BBBBBB // 辅助文字浅灰但饮食页面把A改成了橙色#FF9F43社区页面把A改成了绿色#00B894。这不是主色变了而是每个页面有自己的语义色红色A 运动首页、训练页、详情页橙色A 食物饮食页绿色A 社交社区页这种同一变量名不同页面不同值的做法有风险——如果以后抽公共组件不能假设A一定是红色。更好的做法是定义语义化常量COLOR_EXERCISE、COLOR_FOOD、COLOR_SOCIAL。四、State数组更新的固定模式社区页面的点赞、关注、评论、发帖四个交互都涉及State数组更新。每次更新都用同一个模式// ❌ 不触发UI更新 this.posts[idx].liked true this.posts[idx].likes // ✅ 触发UI更新 this.posts this.posts.slice()ArkUI的State只检测引用变化不检测对象内部属性变化。直接修改数组元素的属性不会触发重新渲染。必须创建新数组slice或用for循环复制让State检测到引用变更。这个模式在社区页面出现了5次点赞、关注×2、评论、发帖是整个App中最重要的技术细节。如果只看一个页面可能觉得slice()是多余的但看过所有页面后会发现——每次涉及State数组更新都必须这样做。五、Builder的三种用途1. UI组件封装Builder RingStat(label, value, unit, color) { ... } // 首页数据环 Builder QuickBtn(icon, label, color, action) { ... } // 首页快捷按钮 Builder NavBtn(idx, label, active) { ... } // 底部导航 Builder Stat(label, value, unit, color) { ... } // 个人中心统计 Builder Menu(label, icon, action) { ... } // 个人中心菜单 Builder SCard(value, unit, label, color) { ... } // 进度页统计卡片Builder把重复的UI结构抽成函数接受参数渲染不同内容。同一个App中RingStat和SCard结构几乎一样数值单位标签但没有复用——因为各自的尺寸和padding不同。过早抽象反而会增加理解成本。2. 列表项模板ForEach(WEEKLY_DATA, (day: ActivityRecord) { // 柱状图的每根柱子 }) ForEach(this.posts, (p: Post) { // 社区页面的每条帖子 })ForEach本身不是Builder但和Builder配合使用——Builder定义一个卡片长什么样ForEach定义渲染多少个。3. 条件分支if (this.items.length 0) { // 空状态 } else { Scroll() { // 数据列表 } }浏览记录和收藏夹用if/else做条件渲染——有数据显示列表无数据显示空状态。这是整个App中唯一用到条件渲染的地方其他页面都是永远有数据。六、路由跳转的两种模式1. 直接跳转router.pushUrl({ url: pages/WorkoutPage })不传参数目标页面从自己的数据源获取数据。首页的快捷按钮、底部导航都用这种模式。2. 带参数跳转router.pushUrl({ url: pages/WorkoutPage, params: { planId: plan.id } as Recordstring, Object })传递参数给目标页面。首页点击训练计划卡片时传planId社区页面点击用户头像时传name和avatar。目标页面用router.getParams() as Recordstring, Object接收参数然后在aboutToAppear中读取。参数类型断言为Record是因为router.getParams()返回object需要手动转型。参数传递的局限ArkUI的路由参数只能传基本类型string/number/boolean和简单对象。不能传函数、不能传复杂嵌套对象。如果需要传大量数据应该用AppStorage或全局状态管理而不是路由参数。七、数据层的接口常量函数三层结构WorkoutData.ets定义了整个App的数据层interface Exercise { ... } // 接口定义数据结构 export const EXERCISES [...] // 常量存储数据 export function getExerciseById(id) { ... } // 函数提供查询这种接口常量函数的三层结构是HarmonyOS轻量级应用的标准数据架构。它不需要数据库、不需要网络请求、不需要状态管理框架——用最简单的方式让页面有数据可渲染。社区页面和饮食页面没有走WorkoutData各自在页面内硬编码数据。这说明WorkoutData不是一个全局数据服务而是运动相关数据服务。如果以后要做数据联动需要把所有数据统一到一个服务中。八、页面间的数据关系首页WEEKLY_DATA[3]→ 步数/卡路里/运动时间 训练页WORKOUT_PLANS→ 计划列表 → 点击跳转详情页 详情页EXERCISES[id]→ 动作步骤 进度页WEEKLY_DATA→ 柱状图 统计卡片 饮食页独立数据→ 已摄入/剩余热量 社区页独立数据→ 帖子列表 个人中心独立数据→ 统计数据首页、训练页、进度页共用WorkoutData的数据——它们之间有数据关联。饮食页、社区页、个人中心各自独立——它们的数据不走WorkoutData。这种部分共用、部分独立的数据关系反映了App的功能分区运动相关功能首页/训练/进度共享数据生活相关功能饮食/社区/个人各自独立。九、贯穿整个App的三个教训1. State数组必须创建新引用社区页面的5次交互证明了这一点直接修改数组元素不触发UI更新必须slice()创建新数组。这是ArkUI和React/Vue的最大区别——React的setState会浅比较ArkUI的State只比较引用。2. 硬编码在原型阶段是合理的WEEKLY_DATA[3]作为今天、1570千卡的total、630千卡的剩余——这些硬编码在原型阶段完全可以接受。它们让页面有数据可渲染让设计师和开发者能讨论UI细节。但要清楚地标记这些是临时方案后续必须替换为动态数据。3. Builder是ArkUI的组件化方案每个页面都定义了2-3个Builder把重复的UI结构封装起来。Builder不是最佳实践而是最简方案——它比直接复制粘贴好但比组件化差。当App需要跨页面复用UI时应该把Builder升级为Component。