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 Record<string, Object>
})
传递参数给目标页面。首页点击训练计划卡片时传planId,社区页面点击用户头像时传name和avatar。
目标页面用router.getParams() as Record<string, 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。
更多推荐
所有评论(0)