哪些结构在每个页面都出现?哪些设计决策贯穿始终?哪些坑踩了不止一次?

把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_EXERCISECOLOR_FOODCOLOR_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。


Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐