HarmonyOS 6 一次开发多端部署实战:用「鱼缸记」做智能家居养护 App

适用活动:鸿蒙生态赋能资源丰富度建设(2026-07-15 ~ 2026-12-31)
主题方向:多设备应用开发 + 应用性能优化 + 上架审核
工程版本:HarmonyOS 6.1.1(API 24),包名 com.personal.yugangji
设备类型:phone / tablet / 2in1

水族养护是典型的「家里用、随时看、不能断网」场景。用户打开 App 的第一诉求不是云端同步,而是:缸里养了什么、今天喂没喂、水质看起来正不正常、勾几个症状马上拿到可执行步骤。

「鱼缸记」按这个场景做成 纯单机 HarmonyOS 应用:不建业务服务器、不登录、不投广告。一套 ArkTS 代码覆盖手机、平板和 2in1,窗口变宽时发生结构变化,而不是把手机稿等比放大。本文按真实工程拆四件事:分层、断点、场景动画性能、上架审核三表对齐。

文中截图均来自本机 HarmonyOS 6.1.1 模拟器实跑(手机 + 三折叠展开),不是效果图。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 1 手机竖屏首页。问候语、25℃ / pH、加鱼 / 喂食 / 灯光都叠在缸景上,没有再套一层底栏。


1. 为什么这个场景适合鸿蒙,而不是再套一层云

智能家居类 App 最容易踩两个坑:

  1. 把记录工具做成商城。卖鱼、卖药、包活承诺,审核和内容合规都会卡住。
  2. 把「拍照」做成「云端诊断」。没有深度合成/生成式 AI 资质,商店简介却写 AI 识别,提审几乎必打回。

鱼缸记把产品边界写死:

不做
演示缸、图鉴、混养提示、日程、打卡规则方案商城、直播、包活、兽医处方
照片仅本机对比,拒绝相册仍可打卡后台偷拍、上传照片、生成式文案
Preferences 落本地,关闭系统云备份账号墙挡住帮助 / 隐私政策

审核主路径也按飞行模式设计:

演示缸 → 日程看到喂食/换水/液肥 → 打卡勾症状 → 立刻显示
「立刻做 / 本周 / 风险」预置方案(规则表,不是大模型)

这不是功能缩水,是把「一次开发多端部署」的代码量和「能过审」绑在同一条需求上。


2. 工程骨架:app / business / foundation

目标 SDK:6.1.1(24)。模块分层:

app → business → component → foundation
page / view → view_model → repository → model
职责禁止
app/pages/Index.etsNavigation 栈、窗口宽度、毛玻璃 Sheet 编排直接读写 Preferences
business/home pedia checkin schedule mine页面与规则同级模块互相 import
component/glass可复用毛玻璃面板夹带业务字段
foundation断点、Preferences、资源图反向依赖 Page

根页面用 Navigation + onAreaChange 拿窗口宽度,所有断点判断都吃同一个 winW,避免每个子页各自 display.getDefaultDisplaySync() 各算一套。

Navigation(this.pathInfos) {
  Stack() {
    HomePage({ vm: this.homeVm, winW: this.winW, onOpenSheet: (id: string) => {
      this.openSheet(id);
    } })
    if (this.sheet.length > 0) {
      GlassPanel({ title: this.sheetTitle(), winW: this.winW, onClose: () => {
        this.closeSheet();
      } }) {
        this.sheetBody()
      }
    }
  }
}
.hideTitleBar(true)
.mode(NavigationMode.Stack)
.onAreaChange((_o: Area, area: Area) => {
  this.winW = area.width as number;
})

窗口宽度是「一次开发多端」的唯一真值。折叠展开、分屏、平板横屏,本质都是 winW 跨过 600 / 840。


3. 多设备:断点要改结构,不要改缩放

官方自适应常用档:sm 320、md 600、lg 840。工程里收成工具类:

export class Breakpoint {
  static readonly SM: number = 320;
  static readonly MD: number = 600;
  static readonly LG: number = 840;

  static isMd(width: number): boolean {
    return width >= Breakpoint.MD;
  }

  static isLg(width: number): boolean {
    return width >= Breakpoint.LG;
  }

  static collectionColumns(width: number, base: number): number {
    if (width >= Breakpoint.LG) {
      return base + 2;
    }
    if (width >= Breakpoint.MD) {
      return base + 1;
    }
    return base;
  }
}

鱼缸记没有「所有设备同一套底栏 + width('100%')」。各页结构变化如下。

3.1 图鉴:手机栈式,平板左右分栏

md 及以上,图鉴 Sheet 变成左列表右详情,选中态用同一个 selectedId,不再 push 一层页面:

} else if (Breakpoint.isMd(this.winW)) {
  Row() {
    PediaListPage({
      vm: this.pediaVm,
      winW: this.winW,
      selectedId: this.selectedId,
      embedded: true,
      onOpen: (id: string) => {
        this.selectedId = id;
      }
    })
    .layoutWeight(1)
    SpeciesDetailPage({
      vm: this.pediaVm,
      speciesId: this.selectedId,
      showBack: false,
      embedded: true
    })
    .layoutWeight(1)
  }
  .width('100%')
  .height('100%')
}

窄屏仍是列表 → 详情两步。这是集合页的标准响应式模式:N 列变 N+1,详情从「下一页」变成「右侧栏」。三折叠展开后实拍如下:左网格选中霓虹灯鱼,右侧立刻出温度 / pH / 体型 / 混养注意,不必再 push 一层页面。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 2 三折叠展开。图鉴 Sheet 左右分栏,选中态用同一 selectedId

3.2 毛玻璃面板:大屏变宽,而不是铺满

GlassPanelbackdropBlur(32) 做系统级模糊,宽度按断点切换,并加 maxWidth: 880,避免 2in1 上出现一条横向拉满的「假平板」对话框。

.width(Breakpoint.isLg(this.winW) ? '72%' : '92%')
.height(Breakpoint.isLg(this.winW) ? '84%' : '78%')
.constraintSize({ maxWidth: 880 })
.backgroundColor($r('app.color.frost_bg'))
.border({ width: 1, color: $r('app.color.frost_border') })
.borderRadius(28)
.backdropBlur(32)

首页状态卡、右侧快捷栏、更多功能菜单走同一套 frosted token,图标优先 SymbolGlyph($r('sys.symbol.xxx')),减少自绘 SVG 在深色水底上的糊边。

3.3 首页安全区与留白

首页缸景全幅铺开,状态卡贴在底部,必须吃系统避让:

.expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.TOP, SafeAreaEdge.BOTTOM])
.padding({
  left: Breakpoint.isLg(this.winW) ? 36 : 18,
  right: Breakpoint.isLg(this.winW) ? 36 : 18,
  top: 18,
  bottom: 18
})

横屏、折叠、平板分屏时,改的是 padding 和面板占比,不改卡片圆角和字体 token。几何锁定、结构可变,这是「一套视觉合同、多套布局」的落地方式。

module.json5 声明三类设备,避免只标 phone 导致平板分发被拒:

"deviceTypes": ["phone", "tablet", "2in1"]

4. 场景动画:为什么量产不用每帧 Canvas 清屏

缸里的鱼要动,这是产品记忆点。工程里曾经写过 Tank3DRendererCanvasRenderingContext2D 每 30ms clearRect 再画渐变、水草、气泡、鱼。量产首页没有挂它。

量产路径是 TankAquarium:水草和鱼都是 Image + position,ViewModel 按窗口矩形做边界反弹。

aboutToAppear(): void {
  this.timer = setInterval(() => {
    this.vm.tickSwim(this.areaW, this.areaH);
  }, 48);
}

aboutToDisappear(): void {
  if (this.timer >= 0) {
    clearInterval(this.timer);
    this.timer = -1;
  }
}

游动逻辑把鱼限制在水面以下、底砂以上,碰到左右边界改 vx 并翻转:

tickSwim(width: number, height: number): void {
  if (width < 40 || height < 80) {
    return;
  }
  const left = 12;
  const right = width - 12;
  const top = height * 0.16;
  const bottom = height * 0.68;
  for (const fish of this.fishes) {
    fish.x = fish.x + fish.vx;
    fish.y = fish.y + Math.sin(fish.x / 36) * 0.35;
    if (fish.x + fish.w > right) {
      fish.vx = -Math.abs(fish.vx);
      fish.flip = true;
      fish.x = right - fish.w;
    }
    // ...
  }
}

对应的性能取舍:

方案帧成本问题
Canvas 全场景重绘(实验代码)每帧清屏 + 渐变 + 多条正弦波沙石用 Math.random() 每帧重生,闪烁且吃 CPU
Image 精灵 + 48ms tick(量产)只改 x/y/flip鱼数量要封顶,水草不参与水平位移

成员重建时直接砍数量:水草最多 6,鱼最多 12。ForEach 用稳定 id 做 key,装饰层 HitTestMode.None,点击穿透到下面的喂食 / 灯光按钮。

ForEach(this.vm.fishes, (item: SwimSprite) => {
  Image(item.image)
    .width(item.w)
    .height(item.h)
    .scale({ x: item.flip ? -1 : 1, y: 1 })
    .position({ x: item.x, y: item.y })
    .hitTestBehavior(HitTestMode.None)
}, (item: SwimSprite) => item.id)

48ms 约 21fps,对「鱼在游」足够,比 30ms 全 Canvas 更省。组件销毁必须 clearInterval,Sheet 反复打开时否则会叠多个定时器。

如果以后要做更接近 3DGS / 端侧重建的缸景,正确入口是独立渲染表面 + 固定随机种子,而不是在 drawScene() 里每帧 Math.random()。鱼缸记把实验渲染器和量产精灵层拆开,就是为了不把帧预算绑死在首页。


5. 数据面:同步 Preferences,异步只做启动灌缓存

单机 App 最怕两件事:首屏空数据、打卡后杀进程记录没了。

LocalStore 启动时把常用键一次灌进内存 Map,读路径全同步;写路径先改缓存,再 putSync + flushSync,失败再走 Promise,避免把 UI 线程卡在首次 getPreferences

static get(key: string, fallback: string): string {
  const hit = LocalStore.cache.get(key);
  if (hit !== undefined) {
    return hit;
  }
  return fallback;
}

static set(key: string, value: string): void {
  LocalStore.cache.set(key, value);
  const p = LocalStore.pref;
  if (p === undefined) {
    return;
  }
  try {
    p.putSync(key, value);
    p.flushSync();
  } catch (_err) {
    p.put(key, value).then(() => {
      p.flush().catch(() => {});
    }).catch(() => {});
  }
}

云备份关闭,卸载即清,和隐私政策一致:

{
  "allowToBackupRestore": false
}

喂食、开灯、换水只在本机打时间戳,文案写明「不控制喂食器 / 灯具 / 过滤硬件」。IoT 控制是下一阶段,不能在 v1 简介里写成「智能硬件联动」。


6. 打卡方案:规则表先于任何「识别」

打卡是审核主路径,必须在拒绝相册时也能走完。CheckinRepository.match 按症状 key 返回三段固定文案,并强制「仅供参考 / 不替代兽医」。

match(selected: string[]): CarePlan {
  let now = '观察水质与温度,记录今日状态。';
  let week = '按日程完成喂食、换水 1/3 与水草液肥。';
  let risk = '建议记录温度、pH、氨氮/亚硝酸盐和溶氧;异常或持续恶化时联系水生动物兽医。';
  if (selected.indexOf('white') >= 0) {
    now = '先记录白点位置与数量,检查温度和水质;不要仅凭照片自行用药或升温。';
    risk = '白点可能有多种原因,治疗方式依病因和物种而异;本应用不作诊断。';
  } else if (selected.indexOf('air') >= 0) {
    now = '立即检查增氧、过滤和水面扰动,并检测溶氧、氨氮和亚硝酸盐。';
  }
  if (selected.length === 0) {
    now = '未勾选症状:维持日常观察即可。';
  }
  return new CarePlan(now, week, risk);
}

相册走 PhotoViewPicker,取消或抛错只改 hint,不阻断 generate()。勾「白点」后点「查看养护建议」,立刻做 / 本周 / 风险三张卡会写进本机日志,不经过网络。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 3 宽屏打卡。左侧照片可跳过,右侧已给出「立刻做 / 本周 / 风险」,文案写明不作诊断。

工程里另有一个 端侧颜色直方图匹配FishRecognizer),用 ImageKit 把 PixelMap 读成 Uint8Array 后对主色做最近邻,不上传、不调云、不生成文字。它不是 Harmony Intelligence 生成式能力,也不能写进商店简介当「AI 识鱼」。

上架时更容易被打回的,不是「有没有识别」,而是 三表不一致

必须互相看见
module.json5 权限相机 / 读图是 inuse,有 reason 文案
应用内隐私政策写清存什么、不上传、不做云端分析
商店简介 / 截图不写「AI 诊断」「到点系统通知」(当前版本日程只在 App 内展示到期项)

鱼缸记隐私页原文强调:不创建账号、不连接业务服务器、选图可拒绝、养护建议仅供参考。帮助页给出飞行模式逐步操作。审核员按帮助页点一遍,就能完成主路径。


7. 上架审核清单(个人开发者可执行)

结合本工程已经踩过的点,提审前至少核对:

  1. 单机声明为真requestPermissions 不要出现 INTERNET;不要在飞行模式弹联网错误。
  2. 演示数据可走完:未授权相机也能打开演示缸、图鉴详情、打卡方案。
  3. 禁止项不上屏:无价格、无购买、无「保证包活」、无登录墙挡住帮助。
  4. 权限最小化:照片选择失败不崩溃;不在后台申请持续相机。
  5. 备份策略与文案一致:已关云备份,就不要写「换机自动恢复」。
  6. 多设备截图是实机:手机竖屏、折叠展开、平板分栏至少各一张,不要用 AI 效果图冒充商店图。
  7. 通知能力别超卖:没申请通知权限,就不要在简介写「到点推送」。
  8. 签名材料不进仓库:发布证书、Profile、密码只放本机或密钥库。

AGC 勾选「单机 APP」时,审核员会用飞行模式验证。把验收步骤写成帮助页,等于把审核脚本内置进产品。


8. 可以复用的结论

  1. 窗口宽度是多端唯一真值。 onAreaChange 一次采集,Sheet、分栏、字号、面板占比全部消费 winW
  2. 断点必须改信息架构。 图鉴 md+ 左右分栏,毛玻璃 lg 限宽,而不是所有设备一张手机卡片拉宽。
  3. 视觉特效让给系统。 backdropBlur + SymbolGlyph 比自绘毛玻璃和 PNG 图标更稳。
  4. 动画先封顶再谈效果。 精灵位移 + 稳定 key + 销毁停表,比每帧 Canvas 清屏更适合首页常驻。
  5. 审核路径等于产品主路径。 规则表出方案、演示缸、应用内隐私政策,比事后补「飞行模式也能用」成本低。

鱼缸记是垂直行业里很小的一个切口:把「记缸」做成可上架的 HarmonyOS 6 应用。多设备、性能和审核不是三篇分开的文章,而是同一套分层在三个方向上的投影。


Logo

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

更多推荐