智能家居 App 做到现在已经覆盖了 5 个页面和 1 个数据服务层。和其他两个 App 相比,它的页面数量最少,但每个页面的信息密度最高——首页塞了五个区块,房间页有三种设备类型的条件渲染,设置页有 12 个菜单项。回过头看整个项目,有些规律在单篇文章里讲不清楚,这篇做一次全局性的梳理。
完整效果
在这里插入图片描述

项目全貌

pages/
  Index.ets           首页仪表盘
  RoomPage.ets        房间详情(设备控制)
  SecurityPage.ets    安防中心
  EnergyPage.ets      能耗监测
  SettingsPage.ets    设置中心

services/
  DeviceData.ets      设备/房间/场景/能耗数据 + 操作函数

5 个页面,1 个服务层。页面数量是三个 App 里最少的(运动健康 12 页、旅行探索 10 页),但数据层是最复杂的——17 个设备、5 个房间、5 个场景、7 条能耗记录,还有 4 个操作函数。

数据层的集中管理

// DeviceData 暴露的内容
export interface Device { ... }
export interface Room { ... }
export interface Scene { ... }
export interface EnergyRecord { ... }

export const ROOMS: Room[] = [...]
export const DEVICES: Device[] = [...]
export const ENERGY_DATA: EnergyRecord[] = [...]
export const SCENES: Scene[] = [...]

export function toggleDevice(id: number): boolean { ... }
export function setDeviceValue(id: number, val: number): void { ... }
export function getDevicesByRoom(room: string): Device[] { ... }
export function getActiveCount(): number { ... }

在这里插入图片描述

四个接口、三组数据、四个操作函数——这是三个 App 里数据层最完整的。运动健康 App 的 WorkoutData 只有数据定义和查询函数,没有操作函数;旅行探索 App 的 TravelData 只有只读的 mock 数据。智能家居 App 的 DeviceData 同时承担了"数据存储"和"数据操作"两个职责。

这种集中管理的好处是所有页面共享同一份数据——首页调用 toggleDevice 修改设备状态后,RoomPage 通过 getDevicesByRoom 读到的是最新数据。代价是模块级常量被直接修改,没有封装保护。在原型阶段这是最简单的方案,后续需要引入服务层模式来规范数据访问。

页面间的数据流

DeviceData (数据源 + 操作函数)
  │
  ├→ Index (首页仪表盘)
  │    ├→ toggleDevice()         切换设备状态
  │    ├→ getActiveCount()       统计在线数
  │    ├→ activateScene()        激活场景
  │    ├→ RoomPage               (传房间名称)
  │    ├→ SecurityPage           (无参数)
  │    ├→ EnergyPage             (无参数)
  │    └→ SettingsPage           (无参数)
  │
  ├→ RoomPage (房间详情)
  │    ├→ getDevicesByRoom()     按房间查询设备
  │    ├→ toggleDevice()         切换设备状态
  │    └→ setDeviceValue()       调节设备参数
  │
  ├→ SecurityPage (安防中心)
  │    └→ 本地 @State            (不依赖 DeviceData)
  │
  ├→ EnergyPage (能耗监测)
  │    └→ ENERGY_DATA            (只读)
  │
  └→ SettingsPage (设置中心)
       └→ 本地 @State            (不依赖 DeviceData)

五个页面对 DeviceData 的依赖程度各不相同:

  • Index:深度依赖——读取所有数据组,调用三个操作函数
  • RoomPage:中度依赖——调用查询和操作函数
  • EnergyPage:轻度依赖——只读取 ENERGY_DATA
  • SecurityPage:无依赖——所有数据在页面内部
  • SettingsPage:无依赖——所有数据在页面内部

这种"核心页面重依赖、边缘页面零依赖"的分布在中小型 App 里很常见。首页和房间页是功能核心,需要访问完整数据;安防、能耗、设置是辅助功能,数据独立或只读。

@State 的三种使用场景

整个项目里的 @State 可以归纳为三种使用场景:

场景一:动态数组(需要复制触发刷新)

// Index - 场景激活
@State scenes: Scene[] = []
this.scenes[idx].active = true
const copy: Scene[] = []
for (let j...) { copy.push(this.scenes[j]) }
this.scenes = copy

// RoomPage - 设备列表刷新
this.devices = getDevicesByRoom(this.room)

数组的修改需要通过"复制替换"或"重新查询"来触发 @State 刷新。这是 ArkTS 状态管理里最常见的坑。

场景二:简单布尔值(直接切换)

// SettingsPage - 开关状态
@State notifyOn: boolean = true
this.notifyOn = !this.notifyOn

// SecurityPage - 安防状态
@State doorLocked: boolean = true
this.doorLocked = !this.doorLocked

布尔值的切换直接赋值就行,不需要复制。@State 能直接检测到基本类型值的变化。

场景三:数值统计(重新计算赋值)

// Index - 在线设备数
@State devCount: number = getActiveCount()
onPageShow(): void { this.devCount = getActiveCount() }

数值型 @State 不需要手动修改,通过重新调用计算函数获取最新值再赋值。

三种场景对应三种不同的刷新策略:

场景 数据类型 刷新方式 复杂度
动态数组 Array 复制替换或重新查询
简单布尔 boolean 直接赋值
数值统计 number 重新计算赋值

理解这三种场景,就掌握了 ArkTS @State 的核心用法。

Builder 的职责分工

整个项目用了 7 个 Builder,每个 Builder 的职责都很单一:

Builder 所在页面 职责 参数数量
NavBtn Index 底部导航按钮 3
StatusCard SecurityPage 安防状态展示 4
ActionBtn SecurityPage 安防操作按钮 3
ECard EnergyPage 能耗统计卡片 4
RoomEnergy EnergyPage 房间能耗条形图 3
Link SettingsPage 跳转链接菜单项 4
Toggle SettingsPage 开关切换菜单项 5

没有跨页面复用的 Builder——每个 Builder 都只在自己的页面里使用。唯一的页面内复用是 SettingsPage 的 Link 和 Toggle(分别被多次调用)。

这和前两个 App 的情况一致。Builder 在 ArkTS 里的定位是"页面内的轻量复用"。当同一个结构在页面里出现两次以上时,抽成 Builder;只出现一次的,内联写就行。

颜色常量的项目间一致性

常量 智能家居 运动健康 旅行探索
A(主色) #6C5CE7 紫 #FF4757 红 #FF6B35 橙
T1 #1E1B2E #1E1B2E #1E1B2E
T2 #888888 #888888 #888888
T3 #BBBBBB #BBBBBB #BBBBBB
BG #F5F4FA #F8F7FC #F8F7FC

T1/T2/T3 在三个 App 里完全一致——它们是"中性色",不承载品牌调性。A(主色)根据 App 主题变化:智能家居用紫色(科技感),运动健康用红色(活力感),旅行探索用橙色(探索感)。

BG(背景色)在智能家居里是 #F5F4FA(偏冷灰),和其他两个 App 的 #F8F7FC(偏暖灰)略有不同。冷灰色和紫色主色搭配更协调,暖灰色和红/橙主色搭配更协调。

导航栏的统一模式

所有页面的导航栏结构完全一致:

Row() {
  Row() { SymbolGlyph($r('sys.symbol.chevron_left')).fontSize(20).fontColor([T1]) }
    .width(34).height(34).borderRadius(17)
    .backgroundColor('rgba(0,0,0,0.03)')
    .justifyContent(FlexAlign.Center)
    .onClick(() => { router.back() })
  Text('页面标题').fontSize(20).fontWeight(FontWeight.Bold)
    .fontColor(T1).margin({ left: 10 }).layoutWeight(1)
}

在这里插入图片描述

34×34 圆形返回按钮 + 标题 + layoutWeight(1) 占满剩余空间。这个结构在 5 个页面里出现了 4 次(Index 用底部导航栏代替)。

统一的导航栏意味着用户在任何页面都知道"左上角是返回按钮"——不需要重新学习每个页面的导航方式。这种一致性比任何单个页面的设计都重要。

条件渲染的两种模式

项目里有两种条件渲染模式:

模式一:if 控制显隐

// RoomPage - 设备关闭时隐藏滑块
if (d.on && d.type === 'light') {
  Slider(...) 
}

// SettingsPage - 无条件渲染所有菜单项

设备页面的滑块根据设备状态和类型决定是否显示——灯关闭时亮度滑块消失,空调关闭时温度滑块消失。这种条件渲染让卡片高度能自适应内容。

模式二:三元表达式控制样式

// SecurityPage - 状态文字颜色
Text(this.doorLocked ? '已锁定' : '未锁定')
  .fontColor(this.doorLocked ? '#00B894' : '#FF6B6B')

// SettingsPage - 开关文字
Text(on ? '🟢 开' : '⚫ 关')
  .fontColor(on ? A : T3)

不需要隐藏/显示元素,只需要根据状态改变文字或颜色时,用三元表达式更简洁。

两种模式的选择标准:需要隐藏元素用 if,只需要改样式用三元表达式

数据修改的两种粒度

项目里的数据修改有两种粒度:

粗粒度:切换布尔状态

toggleDevice(id)     // on: true → false
this.doorLocked = !this.doorLocked
this.alarm = !this.alarm

只修改一个布尔值,不涉及数值计算。

细粒度:调节连续数值

setDeviceValue(id, val)  // value: 80 → 65
d.value = Math.round(v)  // Slider 拖动

修改一个数值参数,需要取整、范围限制等处理。

粗粒度操作(开关、激活/撤防)适合用按钮实现,细粒度操作(亮度、温度)适合用 Slider 实现。页面的交互设计应该根据数据修改的粒度来选择控件类型。

页面信息密度的对比

页面 区块数 @State 数量 Builder 数量 数据来源
Index 5 2 2 DeviceData(深度)
RoomPage 1(列表) 2 0 DeviceData(中度)
SecurityPage 3 3 2 本地
EnergyPage 4 0 2 DeviceData(只读)
SettingsPage 5 5 3 本地

首页的信息密度最高(5 个区块),但 @State 数量不是最多的(2 个)——因为很多数据是直接从 DeviceData 读取的,不需要在页面里维护副本。SettingsPage 的 @State 最多(5 个),因为每个开关都需要独立的状态变量。

这个规律说明:信息密度和状态管理复杂度不是正相关的。首页看起来最复杂,但状态管理相对简单;设置页看起来最简单,但状态变量最多。页面的复杂度取决于它的交互模式,而不是它的视觉复杂度。

Logo

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

更多推荐