HarmonyOS智能家居——从零搭建控制中心的开发实战总结
做了五个页面之后,回过头看整个智能家居项目的开发过程,有一些在单篇讲解里分散的经验值得集中梳理。这篇从数据层设计、页面架构、交互模式三个维度做一次实战总结,重点讲"为什么这样做"而不是"做了什么"。
完整效果
一、数据层设计:为什么选模块级常量
智能家居 App 的数据层是所有页面的基础。设计时面临一个选择:数据存在哪里?
方案一:每个页面各自管理自己的数据。首页的设备列表、房间页的设备列表、安防页的摄像头列表各自独立。好处是简单,坏处是数据不一致——首页关了一个灯,房间页还显示开着。
方案二:用一个全局状态管理库(比如 AppStorage)统一管理。好处是数据一致,坏处是复杂度高,需要学一套新的 API。
方案三:模块级常量 + 操作函数。所有数据在 DeviceData.ets 里定义,所有页面导入同一份数据,操作函数直接修改模块级数组。
最终选了方案三。原因有两个:
第一,智能家居的数据需要跨页面一致。用户在房间页关了一个灯,回到首页必须看到状态变化。模块级常量天然支持这个需求——所有页面读写的是同一份内存数据。
第二,操作函数封装了修改逻辑。toggleDevice 不只是简单地 on = !on,它还返回了新的状态值,调用方可以据此决定要不要刷新 UI。这种"修改 + 返回"的模式比"只修改不返回"更实用。
// toggleDevice 的实现
export function toggleDevice(id: number): boolean {
for (let i: number = 0; i < DEVICES.length; i++) {
if (DEVICES[i].id === id) { DEVICES[i].on = !DEVICES[i].on; return DEVICES[i].on }
}
return false
}

方案三的代价是没有数据封装——任何导入 DeviceData 的页面都能直接修改 DEVICES 数组,不经过操作函数。在 5 个页面的规模下这不是问题,但如果页面增加到 20 个以上,需要考虑引入更严格的数据访问控制。
二、页面架构:五种信息密度的处理方式
五个页面的信息密度差异很大,每种密度需要不同的处理方式:
低密度:安防中心(3 个区块)
导航栏
状态卡片(门锁 + 警报)
摄像头列表(3 项)
快捷操作(3 个按钮)
信息少,布局简单。整个页面不需要滚动——内容在一屏内就能展示完。@State 只有两个布尔值,状态管理最简单。
中密度:能耗监测(4 个区块)
导航栏
统计卡片(3 项)
24小时柱状图
各房间能耗条形图
节能建议
信息量中等,需要用 Scroll 让内容可滚动。没有用户交互——所有数据是只读的,不需要 @State。
高密度:首页仪表盘(5 个区块)
导航栏
Header(问候语 + 头像)
设备状态卡片
房间横向滚动
快捷控制网格
能耗柱状图
智能场景
底部导航栏
信息密度最高,需要在有限的屏幕空间里展示最多的内容。处理策略是"分层展示"——最重要的信息(设备状态卡片)放在最显眼的位置(顶部大面积渐变),次要信息(能耗、场景)放在下方,用横向滚动节省垂直空间。
交互密度高:房间详情页
导航栏
设备列表(每个设备可能有滑块)
区块数量少,但每个设备卡片的交互复杂度高——开关切换、亮度滑块、温度滑块。处理策略是"条件渲染"——设备关闭时只显示基本信息,开启后展开滑块,卡片高度自适应。
菜单密度高:设置中心
导航栏
用户卡片
设备管理(3 项)
偏好设置(5 项)
系统(4 项)
退出登录
12 个菜单项,5 个开关,4 个跳转链接。处理策略是"分组 + 复用"——用两套 Builder(Link 和 Toggle)处理两种菜单形态,用 Div 分割线分组。
五种密度对应五种处理策略,没有"一刀切"的方案。判断页面信息密度后选择对应的策略,比套用固定模板更有效。
三、交互模式:四种设备控制方式
智能家居 App 里的设备控制有四种方式,每种对应不同的数据修改粒度:
方式一:布尔开关(toggleDevice)
.onClick(() => { toggleDevice(1); this.devCount = getActiveCount() })

最简单的控制方式——点击一下切换开/关。适用于灯、电视、音箱等不需要参数调节的设备。操作后需要重新计算在线设备数。
方式二:数值滑块(setDeviceValue)
Slider({ value: d.value, min: 1, max: 100, step: 1 })
.onChange((v: number) => {
d.value = Math.round(v)
this.devices = getDevicesByRoom(this.room)
})
适用于需要精确控制数值的设备——灯的亮度、空调的温度。Slider 的 range 和 step 根据设备类型设定:亮度 1-100 步长 1,温度 16-30 步长 1。
方式三:场景激活(activateScene)
private activateScene(idx: number): void {
for (let i...) { this.scenes[i].active = false }
this.scenes[idx].active = true
// 复制数组触发刷新
}
一键触发多个设备的联动操作。排他逻辑保证同一时间只有一个场景运行。操作后需要复制数组触发 @State 刷新。
方式四:安防状态切换(本地 @State)
this.doorLocked = !this.doorLocked
this.alarm = !this.alarm
最轻量的控制方式——直接切换布尔值,不需要调用操作函数,不需要重新查询数组。适用于安防、设置等独立页面的本地状态。
四种方式的共同点是"操作后刷新 UI",区别在于刷新策略:
| 方式 | 数据修改 | 刷新策略 | 复杂度 |
|---|---|---|---|
| 布尔开关 | toggleDevice | 重新统计在线数 | 低 |
| 数值滑块 | setDeviceValue | 重新查询房间设备 | 中 |
| 场景激活 | 直接修改 active | 复制数组替换引用 | 高 |
| 安防切换 | 直接修改 boolean | 无需额外刷新 | 低 |
四、颜色编码的系统化
整个项目的颜色不是随便选的,有一套系统化的编码规则:
功能颜色
| 颜色 | 值 | 含义 | 使用场景 |
|---|---|---|---|
| 主色 | #6C5CE7 | 科技感、品牌色 | 渐变背景、强调文字 |
| 绿色 | #00B894 | 安全、在线、正常 | 门锁已锁定、设备在线 |
| 红色 | #FF6B6B | 警告、离线、不安全 | 门锁未锁定、设备离线 |
| 橙色 | #FF9F43 | 预警、暖色强调 | 本月预估用电、空调滑块 |
层次颜色
| 颜色 | 值 | 用途 |
|---|---|---|
| T1 | #1E1B2E | 标题文字(最高层级) |
| T2 | #888888 | 副文字(中间层级) |
| T3 | #BBBBBB | 辅助灰(最低层级) |
| BG | #F5F4FA | 页面背景 |
| CW | #FFFFFF | 卡片背景 |
功能颜色编码了"状态含义"——用户看到绿色就知道"正常",看到红色就知道"有问题"。层次颜色编码了"信息层级"——T1 最重要,T2 次之,T3 最弱。两套颜色系统互不干扰,共同构成了页面的视觉秩序。
颜色透明度的运用
项目里大量使用了颜色 + 透明度的组合:
A + '25' // 主色 14% 透明度 → 投影颜色
A + '30' // 主色 18% 透明度 → 房间卡片边框
s.color + '15' // 场景色 8% 透明度 → 场景卡片背景
'rgba(0,0,0,0.03)' // 黑色 3% 透明度 → 返回按钮背景
透明度让颜色更柔和——纯色太强烈,加了透明度后变成"淡淡的色调",适合做大面积的背景色或边框色。
五、@State 刷新的三种模式
整个项目里 @State 的刷新可以归纳为三种模式:
模式一:重新查询赋值
// RoomPage - 切换设备后
this.devices = getDevicesByRoom(this.room)
// Index - 切换场景后
this.scenes = copy // 复制后的新数组
适用于数组类型的 @State。修改源数据后,通过重新查询或复制得到新数组,替换 @State 的引用。ArkTS 检测到引用变化后触发重新渲染。
模式二:重新计算赋值
// Index - 设备状态变化后
this.devCount = getActiveCount()
适用于数值类型的 @State。修改源数据后,重新调用计算函数获取最新值,赋值给 @State。不需要复制数组,直接赋值就行。
模式三:直接赋值
// SettingsPage - 开关切换
this.notifyOn = !this.notifyOn
// SecurityPage - 安防状态
this.doorLocked = !this.doorLocked
适用于布尔类型的 @State。基本类型值的直接赋值会被 @State 检测到,不需要额外处理。
三种模式的判断标准:数组用模式一,数值用模式二,布尔用模式三。记住这个规则就覆盖了 90% 的 @State 刷新场景。
六、Builder 的设计原则
整个项目用了 7 个 Builder,总结出三条设计原则:
原则一:只抽出现两次以上的结构
如果一个结构只在页面里出现一次,内联写就行,不需要抽 Builder。出现两次以上时再抽取——过早抽取会让代码变得碎片化。
原则二:参数只传"会变的"
// 好的:只传会变的参数
@Builder ActionBtn(icon: string, label: string, action: () => void)
// 不好的:传了不会变的样式参数
@Builder ActionBtn(icon: string, label: string, color: string, fontSize: number, action: () => void)
样式参数(颜色、字号、间距)通常在页面内是固定的,不需要作为 Builder 的参数。只有"不同实例之间会不同"的值才应该传参。
原则三:回调参数用 () => void
当 Builder 需要执行不同操作时,用 action: () => void 回调参数,而不是在 Builder 内部硬编码操作逻辑。这让 Builder 保持通用性,调用方决定"点了之后做什么"。
七、项目结构的组织方式
entry/src/main/ets/
pages/
Index.ets 首页(入口 + 五个区块)
RoomPage.ets 房间详情(设备控制)
SecurityPage.ets 安防中心(状态展示)
EnergyPage.ets 能耗监测(数据可视化)
SettingsPage.ets 设置中心(菜单列表)
services/
DeviceData.ets 数据定义 + 操作函数
页面按功能命名,一目了然。服务层只有一个文件,因为数据量不大,不需要拆分。
文件命名的规则:页面用 XxxPage.ets,服务层用 XxxData.ets 或 XxxService.ets。这种命名约定让打开文件目录就能知道每个文件的职责。
如果后续页面增加,可以考虑按功能模块拆分文件夹:
pages/
home/Index.ets
room/RoomPage.ets
security/SecurityPage.ets
...
但在 5 个页面的规模下,平铺结构更简单,不需要提前设计文件夹层级。
八、开发顺序的建议
如果从零开始做这个项目,建议的开发顺序是:
- DeviceData.ets:先把数据层搭好,定义接口和 mock 数据
- Index.ets:首页是入口,先做出来能看到整体效果
- RoomPage.ets:最核心的功能页面,设备控制在这里
- SecurityPage.ets:最简单的页面,验证 @State 基本用法
- EnergyPage.ets:纯展示页面,验证数据可视化布局
- SettingsPage.ets:菜单最多的页面,验证 Builder 复用
先做数据层再做页面,先做核心功能再做辅助功能。每做完一个页面就预览一次,确认 UI 效果后再做下一个。不要试图一次性把所有页面都写完——分步开发能让你在早期发现问题,避免后期大规模返工。
更多推荐
所有评论(0)