请添加图片描述

1. 前言

我想把阳台上的薄荷放到手机桌面。不是随时观察它的摄像头,而是一张小卡片:显示植物名称、最近一次观察,留一个写日记的入口。闲下来点一下花盆,瓢虫从叶后探出来,过一会儿再回去。

这个需求里有两种不同的时间尺度。观察日记可能几天才更新一次,瓢虫的动作只持续几秒。把它们放在同一张卡片上很自然;让它们遵循同一套加载节奏,却容易出问题。

考虑一种实现:点击花盆后,先读取植物资料,读完再创建动效页面。资料返回得快时,这个依赖不显眼。一旦读取变慢,用户就可能面对一段空白。此时值得追问的并不是如何把读取再加快一点,而是:画出花盆,究竟需要等待哪些资料?

本系列使用一张2×2 ArkTS动态卡片,固定绑定薄荷 plant-001。花盆、叶片和瓢虫都是本地资源,观察日记由主应用手动维护。本篇先处理点击激活与双态加载。摇一摇、出框和倾斜控制会在后续逐步加入。

请添加图片描述

2. 方案

场景动效卡片有普通态和激活态两套UI。普通态使用普通卡片页面,激活态由 LiveFormExtensionAbility 加载页面。这里的普通态也叫非激活态,它仍然可以是一张动态卡片;“非激活”描述当前状态,不是在指定静态卡片类型。

两套UI首先带来一个职责划分。普通态负责用户每天需要的信息:这是什么植物,最近记录了什么,在哪里继续记录。激活态负责短时反馈:让同一盆薄荷里的瓢虫动起来。即使动效不能启动,前一种职责也必须成立。

双态机制并非HarmonyOS 7.0才出现。场景动效配置从API 20已有,本系列后续使用的 triggerTypes 中的 shake 才是API26增量。把版本边界说清楚,才能知道一个问题属于基础接入,还是新增入口。

我把参与者分成四部分。主应用管理植物和日记;FormExtensionAbility 提供普通卡片数据、接收卡片消息;普通卡片页面呈现日常信息;LiveFormExtensionAbility 加载激活页面。这个划分描述责任,不意味着它们共享一个运行环境。

主应用与普通卡片提供方存在进程边界。把当前植物放在主应用的全局变量里,并不能让另一个进程自动读到它。业务数据需要明确的持久化和读取路径。LiveForm内部使用的 LocalStorage 则服务当前会话,不承担跨进程数据库的职责。

请添加图片描述

有了这些边界,加载方案便容易比较。

方案进入激活态时的顺序业务数据迟到时
A等待植物资料,再加载UI页面加载一起被推迟
B同步加载默认UI,再读取资料先提供可显示内容,随后补齐

我选择B。在 onLiveFormCreate 中同步调用 session.loadContent。数据库和网络等异步获取应放在后面。A因此只作为检验加载顺序的反例,不进入正常路径。

“默认UI”也需要认真定义。它应当包含本地花盆和叶片、已知的薄荷身份,以及明确的数据状态。读取过程中写“正在读取最近观察”;读取完成且确实没有日记,才写“还没有观察记录”。这两个状态如果混在一起,用户看到的内容虽然不空白,却可能不准确。

3. 实现

先让配置形成一条可以逐项核对的引用链。本文将普通页面命名为 MintCard,激活页面命名为 MintLive,对应扩展分别命名为 MintFormAbility 和 MintLiveAbility。这些是项目名称,后面的配置与图稿都沿用它们。

位置需要核对的关系
模块声明MintFormAbility 对应普通卡片提供方
卡片配置普通卡片页面指向 MintCard
模块声明MintLiveAbility 的类型为 liveForm
场景动效配置abilityName 指向 MintLiveAbility
页面及资源登记MintLive 页面路径与本地资源实际存在

卡片配置中的关键片段很短,将它合入现有卡片定义即可:

{
  "sceneAnimationParams": {
    "abilityName": "MintLiveAbility"
  }
}

这里暂不添加摇动配置,也不混入 funInteractionParams。两者同时存在会影响系统对卡片类型的识别。声明名称正确、页面路径有效、资源可以解析,比先写一段漂亮动画更值得优先检查。

请添加图片描述

点击链路从普通卡片的动效区域开始,经 postCardAction 的 message 到达 onFormEvent,再由提供方请求 requestOverflow。日记按钮另设区域,目标始终是 plant-001。不要先把整张卡片绑定成动效按钮,再期待局部按钮自行解决事件冲突。

页面加载之前,需要准备一份很小的显示状态。下面的类型属于应用模型,只包含本篇需要的字段。

interface PlantSnapshot {
  plantId: string;
  name: string;
  latestObservation: string;
}

interface MintViewState {
  plantId: string;
  name: string;
  summary: string;
}

function initialMintState(): MintViewState {
  return {
    plantId: 'plant-001',
    name: '薄荷',
    summary: '正在读取最近观察'
  };
}

function toMintView(data: PlantSnapshot): MintViewState {
  return {
    plantId: data.plantId,
    name: data.name,
    summary: data.latestObservation.length > 0
      ? data.latestObservation : '还没有观察记录'
  };
}

这份状态有意没有“已浇水”和“生长进度”。点击瓢虫只是装饰交互,不会产生新的植物事实。普通态和激活态都应遵循同样的内容规则,不能一边展示真实日记,另一边为了画面完整而填入一条观察。

初始化还需要当前卡片的 formId、rect 和圆角信息。formId标识卡片实例,plantId标识业务对象,二者不能互换。rect用于确定原卡片在激活UI里的位置和尺寸,也不能直接替换成窗口绝对坐标。

接入层从当前SDK的回调参数中提取这些值,按页面约定放入当前会话的 LocalStorage。具体参数类型与圆角字段取自对应SDK声明。下列代码展示 onLiveFormCreate 内的关键顺序;其中 initialStorage 是已准备好的会话存储,loadSnapshot和 applySnapshot是项目自己的数据读取与页面更新函数。

// 位于 onLiveFormCreate 的同步执行路径中。
session.loadContent('pages/MintLive', initialStorage);

// 发起业务读取放在加载调用之后。
loadSnapshot('plant-001')
  .then(applySnapshot)
  .catch(showReadFailure);

这里最重要的性质是:loadContent之前没有等待业务读取的 await,也没有将加载调用放进Promise回调。异步函数执行得再快,也不等于满足同步加载约束。

请添加图片描述

接下来才是异步结果的处理。把它收在一个应用层对象里,可以使两个条件变得显眼:结果属于当前植物,而且接收它的会话还活着。

class MintSessionState {
  private active: boolean = true;
  private storage: LocalStorage;

  constructor(storage: LocalStorage) {
    this.storage = storage;
  }

  accept(data: PlantSnapshot | undefined): void {
    if (!this.active) {
      return;
    }
    if (!data || data.plantId !== 'plant-001') {
      this.storage.set('summary', '植物资料不可用');
      return;
    }
    const view = toMintView(data);
    this.storage.set('name', view.name);
    this.storage.set('summary', view.summary);
  }

  fail(): void {
    if (this.active) {
      this.storage.set('summary', '暂时无法读取最近观察');
    }
  }

  close(): void {
    this.active = false;
  }
}

初始化时须先创建 name和 summary等存储项,页面再通过对应的LocalStorage状态绑定读取它们;仅修改一个与页面无关的普通对象,不会建立更新关系。MintSessionState应按会话创建,并在对应销毁路径调用 close,不能让多次激活共用一个长期存活的接收器。

这个检查没有取消底层读取,它只阻止迟到结果继续修改已经结束的页面。取消任务、释放订阅与完整中断处理,还需要按各自资源的生命周期接入。

植物名称和日记需要持久化,瓢虫位置与本次动效进度则不需要。它们随着会话开始而初始化,随着会话结束而丢弃。每次激活都从叶后探出来,比恢复上次被打断时的半截姿态更容易解释。

单次场景动效申请最长3500ms,而且可能提前中断。因此,资料读取完成不应成为动效结束的前提。动效请求失败时,普通卡片仍保留摘要和日记入口;同步加载后仍然空白,则继续检查页面声明、资源和加载错误。改变调用顺序解决的是一个特定依赖,不是所有白屏原因。

4. 检验

检验首先要隔离变量。准备A、B两个配置,使用同一台支持场景动效的HarmonyOS 7/API26设备、同一组资源、同一份薄荷资料和同一个点击入口,只改变加载顺序。每组记录构建编号,避免把两个不同版本的页面效果拿来比较。

读取延迟应当可控制。下面的函数是测试辅助代码;将它包装在同一资料读取入口上,就能分别注入0ms与1000ms的额外等待。

function delay(ms: number): Promise<void> {
  return new Promise<void>((resolve) => {
    setTimeout(() => resolve(), ms);
  });
}

async function readWithDelay(
  read: () => Promise<PlantSnapshot | undefined>,
  delayMs: number
): Promise<PlantSnapshot | undefined> {
  await delay(delayMs);
  return read();
}

1000ms只是实验输入,不代表系统阈值。0ms也只表示没有额外的等待预算,不会把异步读取变成同步操作。A让加载等待这个函数返回,B先完成同步加载调用,再启动读取。这样,两组差别才能落在要检验的依赖上。

每个条件重复三次,保留全部日志和录屏。操作顺序固定为:准备资料状态,确认桌面上的薄荷卡片,开始录屏,点击动效区域,等待退出,再检查普通态。检查主应用未打开的场景时,不要将“没有打开应用页面”扩大解释为所有相关进程均未运行。

用例需要观察的问题
首次添加后激活普通态内容是否完整,两态是否属于同一薄荷
主应用UI未打开是否依赖主应用中的临时变量
注入读取延迟默认画面能否在资料到达前显示
资料缺失是否显示缺省内容,而非编造观察摘要
读取失败是否保留花盆与可理解的错误状态
退出后再次激活是否重新初始化当前会话,避免旧结果串入

请添加图片描述

我会把加载日志与画面证据分开保存。日志至少包含设备、系统、SDK、构建编号、A/B配置、formId、plantId、应用自定义会话ID、事件序号和时间戳。会话ID只是为了关联记录,不是系统规定的字段。

下面是一份记录模板,时间由运行时填写,不预先指定数据到达和画面显示的间隔。

env: device=... os=... sdk=... build=... variant=B
context: formId=... plantId=plant-001 runId=...
seq=1 event=create_enter time=...
seq=2 event=load_call time=...
seq=3 event=read_start time=...
seq=4 event=read_resolved time=... outcome=...
seq=5 event=apply_attempt time=... active=...
video: file=... first_visible_frame=... fps=...

load_call只能证明程序走到了加载调用,不能证明屏幕已经出现薄荷。首帧需要在录屏中独立识别。录屏帧率和日志时间对齐都会影响精度;若没有共同时间基准,就先比较事件顺序与可见状态,不贸然给出毫秒级耗时。

请添加图片描述

这组检验要回答的是:在相同读取延迟下,B是否能够先显示默认UI,随后补齐摘要。如果日志顺序正确而画面依旧空白,就继续核对页面能否加载、资源是否存在以及错误信息。由于这里读取的是本地资料,也没有理由一见空白就归咎于网络。

最终记录应同时保留预期、实际表现和证据文件,未执行的用例留待验证。设备能够显示2×2卡片,并不自动说明它支持场景动效。一次请求成功,也不能证明用户看完了整段动画。

5. 结尾

小花园需要两套界面,是因为日常信息和短时动效有不同的职责。两者共用植物身份、内容规则与视觉资源,各自负责呈现。同步加载则把初始画面从业务读取中分离出来,让资料迟到成为可以处理的状态。

沿着这条路径,我们得到双态职责图、页面与配置清单、加载时序、延迟注入方法和日志模板。它们共同约束同一个问题:页面的出现不应等待它此刻尚不需要的数据。

下一篇保留这张薄荷卡片和两套UI,只增加API26的摇动入口,并区分摇动、点击与普通更新。入口会增加,薄荷仍是 plant-001,观察日记仍由用户亲手记录。

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐