前言

上一篇讲了喵汪星球的整体架构。这一篇进入页面层,重点拆解 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 即可。
  • 适合业务闭环还在快速变化的阶段。

后续如果页面继续增多,再迁移到 Navigationrouter 也不迟。

用 @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 的离线状态,并做好字段兼容和恢复逻辑。

Logo

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

更多推荐