HarmonyOS 技术实战 02:ArkUI 状态驱动首页与五 Tab 导航
前言
上一篇讲了喵汪星球的整体架构。这一篇进入页面层,重点拆解 ArkUI 中一个非常实用的实现:不用复杂路由,先用状态驱动一个五 Tab 主界面。
项目当前主页面在 entry/src/main/ets/pages/Index.ets,包含首页、记录、翻译器、健康、我的,以及一个从首页快速进入的陪玩模块。它不是简单 TabView,而是围绕业务闭环定制了底部导航、中间突出按钮、页面滚动归位和卡片跳转联动。
为什么 MVP 阶段不用复杂路由
很多应用一开始就拆很多页面,但 MVP 阶段常见问题是:路由层复杂了,状态同步反而更容易出错。这个项目的首版目标是验证“宠物档案 + 照护打卡 + 健康应急 + 叫声分析”的完整闭环,因此采用了更直接的结构:
build() {
Column() {
Scroll(this.pageScroller) {
Column() {
if (this.currentTab === '首页') {
this.HomePage()
} else if (this.currentTab === '我的') {
this.ProfilePage()
} else if (this.currentTab === '记录') {
this.ReminderPage()
} else if (this.currentTab === '健康') {
this.HealthPage()
} else if (this.currentTab === '陪玩') {
this.PlayPage()
} else {
this.TranslatorPage()
}
}
}
this.BottomNav()
}
}
这段代码的思路很朴素:currentTab 是唯一页面选择状态,Builder 方法负责渲染对应模块。它的优势是:
- 页面切换成本低,不需要传复杂路由参数。
- 各模块可以直接共享当前宠物、任务和记录状态。
- 桌面卡片跳转时只要改
currentTab即可。 - 适合业务闭环还在快速变化的阶段。
后续如果页面继续增多,再迁移到 Navigation 或 router 也不迟。
用 @StorageLink 处理跨入口 Tab 切换
项目里最值得学习的是这行:
@StorageLink('mwCurrentTab') currentTab: string = '首页'
普通 @State 只在组件内部响应变化,而 @StorageLink 可以和 AppStorage 建立连接。桌面卡片点击后,EntryAbility 会读取 Want 参数,然后写入同一个 key:
private applyCardNavigation(want: Want): void {
const params = want.parameters
if (params === undefined) {
return
}
const rawTab = params['mwTab']
if (typeof rawTab === 'string' && CARD_TABS.indexOf(rawTab) >= 0) {
AppStorage.setOrCreate<string>('mwCurrentTab', rawTab)
}
}
这样就形成了一个很轻的联动链路:
桌面卡片点击
-> postCardAction 携带 mwTab
-> EntryAbility 收到 Want
-> 写入 AppStorage
-> Index.ets 的 @StorageLink 更新
-> 主页面切到对应 Tab
这比在卡片里做复杂业务跳转更稳。卡片只负责告诉主应用“我要去哪”,真正的业务页面仍由主应用控制。
底部导航的设计
底部导航不是简单平均五等分。翻译器是本 App 的特色功能,所以项目把它做成中间突出按钮:
@Builder
BottomNav() {
Stack({ alignContent: Alignment.Top }) {
Row() {
this.NavItem('首页', $r('app.media.tab_home'))
this.NavItem('记录', $r('app.media.tab_record'))
Column().width(78)
this.NavItem('健康', $r('app.media.tab_health'))
this.NavItem('我的', $r('app.media.tab_profile'))
}
Column({ space: Theme.spaceXS }) {
Stack() {
Image($r('app.media.tab_translator_light'))
}
.width(62)
.height(62)
.backgroundColor(Theme.primary)
.borderRadius(31)
Text('翻译器')
}
.onClick(() => {
this.switchTab('翻译器')
})
}
}
这里的技巧是:Row 中间留一个固定宽度占位,再用 Stack 覆盖一个居中的圆形按钮。视觉上像原生 App 常见的“突出主功能入口”,实现上仍然是普通 ArkUI 组件。
切换 Tab 时滚动归位
如果每个 Tab 共用一个 Scroll,切换页面时很容易出现一个问题:用户在健康页滚到底,再切到首页,首页也停在很靠下的位置。
项目用 Scroller 解决:
private pageScroller: Scroller = new Scroller()
private switchTab(tab: string): void {
this.currentTab = tab
this.pageScroller.scrollTo({ xOffset: 0, yOffset: 0 })
}
这是一个小细节,但用户体验差别很明显。尤其是记录页、健康页、我的页都有较长内容时,切换归位会让应用显得更稳。
首页不是信息堆叠,而是任务入口
首页的目标不是展示所有信息,而是回答用户打开 App 时最关心的三个问题:
- 今天下一件事是什么?
- 有没有未完成或逾期事项?
- 我可以快速做什么?
项目中首页通过 nextTask()、overdueText()、QuickActionGrid() 组织信息。快速操作入口包括记录、陪玩、症状、应急:
private readonly quickActions: QuickAction[] = [
{ title: '记录', hint: '吃喝拉撒', tab: '记录', tone: 'primary' },
{ title: '陪玩', hint: '今天玩什么', tab: '陪玩', tone: 'warm' },
{ title: '症状', hint: '快速判断', tab: '健康', tone: 'blue' },
{ title: '应急', hint: '就医准备', tab: '健康', tone: 'danger' }
]
这个数组既是页面数据,也是交互配置。每个入口知道自己要跳到哪个 Tab,样式由 tone 决定。
Builder 方法让页面保持可读
Index.ets 中大量使用 @Builder:
@Builder
HomePage() {}
@Builder
ReminderPage() {}
@Builder
HealthPage() {}
@Builder
TranslatorPage() {}
@Builder
TaskRow(item: CareTask) {}
@Builder
EntertainmentNotice(message: string) {}
这种写法有两个好处:
第一,把页面结构拆成语义块。比如 HealthPage() 负责健康页,SymptomChip() 负责症状标签,InfoRow() 负责信息行。
第二,减少重复 UI。提醒行、状态卡、标签、反馈按钮都可以复用 Builder 方法。
当然,如果组件继续膨胀,下一步应该拆到 components/ 目录。但在单文件 MVP 阶段,Builder 是非常轻量的分层方式。
响应式布局的小处理
项目通过 onAreaChange 获取视口宽度:
.onAreaChange((_oldArea, newArea) => {
this.updateViewportWidth(newArea.width.toString())
})
然后用方法控制页面边距:
private pageHorizontalPadding(): number {
if (this.isLargeScreen()) {
return 56
}
return Theme.spaceL
}
这比写死 16vp 更适合手机、平板、2in1 多设备。喵汪星球的 module.json5 里声明了:
"deviceTypes": ["phone", "tablet", "2in1"]
既然设备类型支持更广,页面宽度就不能只按手机思路设计。
本篇小结
这一篇我们看到了 ArkUI 页面组织中的几个实战点:
- 用
currentTab做单页多模块渲染。 - 用
@StorageLink + AppStorage接收卡片跳转。 - 用 Stack 定制中间突出的主功能按钮。
- 用 Scroller 在切换 Tab 时滚动归位。
- 用 Builder 方法拆出页面、行、卡片、标签等结构。
- 用视口宽度做简单响应式适配。
下一篇进入数据层:如何用 Preferences 保存一个复杂 App 的离线状态,并做好字段兼容和恢复逻辑。
更多推荐



所有评论(0)