组件在鸿蒙生态市场通过审核、正式上架的那一刻,我坐在工位盯着状态栏看了好半天。前后折腾了快三周,从最初手搓的一个Demo,到补齐数据通道、刷新策略、多尺寸适配,再到走完签名、资质、审核的全套流程,这个“活跃预览组件”总算是落地了。我知道很多人一听到“实时预览”“桌面动态卡片”就下意识觉得复杂,其实拆开看,核心就三件事:数据怎么来、界面怎么画、系统怎么让这块卡片在合适的时间更新。我踩过不少坑,也推翻过一版设计,这篇就当是给同样在做鸿蒙卡片类组件、或者正打算把组件提上生态市场的开发者一份实操记录。
这篇内容适合两类人看:一种是刚开始接触鸿蒙卡片开发、想知道实时进度类组件从设计到实现完整链路的新手;另一种是已经写完组件、正卡在上架环节,想知道提审材料怎么组织、哪些驳回理由最常见的老手。我不会贴一堆流水账式的官方文档,只讲实际开发和上架过程中真正影响成败的细节。
1. 为什么我非要做这个“活跃预览组件”
1.1 用户痛点:一条配送状态的“四步查看路径”
先说需求来源。我当时在做一个生活服务类模块,里面涉及外卖配送、快递揽收这类需要持续关注状态变化的场景。本来产品设计的交互很常规:用户打开App,进到订单详情页,看进度,退出。直到有次我拿着测试机下楼拿外卖,手机揣兜里,路过门禁,掏出手机,亮屏,解锁,进App,点订单,等接口返回,才看到骑手还有三分钟到。整个流程下来十几秒,而且等我看到的时候,配送时间又变了。
这种体验放在今天显然跟不上节奏。用户真正需要的是“扫一眼就知道现在到哪一步了”的轻量信息获取方式,而不是每次都要经过解锁、进App、刷新、查找这个四步路径。桌面或锁屏上的一张动态卡片,恰好能把路径压缩成“亮屏即见”。组件本身不替代完整业务,它只解决用户高频关注的进度查询需求,把低频入口变成零成本入口。
1.2 需求拆解:非侵入、实时、低耗电三项硬指标
需求听起来简单,但“实时进度”四个字背后是一组很硬的约束。做组件类功能最忌讳的就是做成一个花架子,所以我当时先给这个活跃预览组件定了三条指标,后面所有技术选型都围绕这三条走。
第一条是非侵入。组件不能强迫用户解锁后还要额外操作,也不能在通知栏频繁弹提醒骚扰用户。信息要“在但不吵”,用户不想看的时候它可以安安静静待在桌面角落里。
第二条是实时性。进度类信息的核心价值是“准”,如果卡片上显示骑手还有两公里,实际人已经在楼下了,这卡片还不如不显示。我当时定的标准是:状态变化到界面更新的延迟不超过30秒,且不能让用户看到明显跳变。
第三条是低耗电。这是最容易翻车的一条。卡片在桌面上常驻,如果底层逻辑做成了高频轮询,半小时就能把用户电量吃掉一截。定了目标:单日后台刷新带来的额外耗电增量控制在整机日常耗电的百分之三以内。
这三条指标看起来互相打架:既要持续更新,又不能频繁刷新;既要实时准确,又得克制省电。后面所有架构设计的取舍,本质上都是在调这三者之间的矛盾。
1.3 为什么通知横幅和普通小组件都扛不住
立项评审时也有人提过:直接用通知推送,或者做一张普通小组件不就行了,何必单独做活跃预览组件。我当初也犹豫过,后来写了两个原型对比,才发现各有各的硬伤。
通知横幅的问题是打断感太强。配送中每二十分钟推一条“您的订单已送达附近”可能还好,但真要按事件粒度推送,用户可能一天收十几条通知,锁屏界面直接被刷屏。而且通知是系统消息的语义层级,用户长期收到的全是来自同一个App的进度提醒,很快就会关掉通知权限,反而把真正重要的提醒一起关没了。
普通小组件的核心缺陷则是“静态”。系统默认小组件大多依赖定时刷新,最短刷新周期也有明显限制,而且刷新时机受系统调度策略影响很大。做纯天气、时钟、日历这类信息变化不频繁的场景还行,做需要秒级或者十秒级响应的配送进度就不够用了。我实测过,静态组件在配送状态切换后,界面更新经常滞后好几分钟,用户体验直接崩塌。
活跃预览组件走的是一条中间路线:它像小组件一样在桌面常驻、不打扰用户,同时又通过事件驱动机制在后台拿到状态变更后主动刷新界面,把更新延迟压到可控范围内。这也是我最终决定自研组件而不是套用现成方案的根本原因。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 组件形态与链路设计:实时卡片背后的数据通道
2.1 卡片形态:基于Stage模型的组件渲染
确定了做组件,第一步是定形态。鸿蒙生态下前端表现层我选的是基于Stage模型的卡片框架,渲染层用ArkTS声明式UI来描述。
为什么选声明式而不是命令式?因为进度类组件的状态变化非常多,如果每次状态更新都要手写指令去改某个视图节点的属性,代码会膨胀到没法维护。声明式的核心好处是:你只需要维护一份数据源,数据变化后框架自动重绘界面。写卡片就像在描述“当数据处于什么状态时界面长什么样”,而不是“现在请你把进度条挪到百分之多少”。
卡片本身布局不复杂,核心就几个元素:状态文字、进度条、预计时间、更新时间。但要把这些元素组织得在2x2、2x4、宽卡片等不同尺寸下都好看,需要在布局层面预留好伸缩策略。这部分我放在第三章细说。
2.2 数据链路:本地状态与远端接口的分工
组件本身不直接拥有完整的业务数据,它只是一个“展示层”,背后需要稳定的数据输入。我在设计时把数据链路拆成了两条支线。
第一条是本地态数据。比如下载进度、计时器、本机任务执行状态,这类数据由设备自身产生,不依赖网络。组件可以直接从本机服务进程订阅,事件一到立刻刷新,延迟可以压到秒级以内。
第二条是远端态数据。比如外卖配送轨迹、快递物流节点、赛事比分,这类数据在服务器上,组件必须通过网络拿。远端数据不能直接轮询拉取,因为轮询既慢又费电,也不能裸奔常驻长连接,因为长连接本身就是一个耗电和稳定性风险点。
实际工程中我两套并行:本地态走事件总线订阅,远端态走一个轻量的更新通道,具体策略见2.4。两条支线最终汇聚成一个统一的数据源对象,渲染层只认这一个对象,不关心数据到底是本机来的还是网络来的。这个抽象带来一个很实际的好处:后面如果我要换远端实现方案,界面一层完全不用动。
2.3 生命周期:创建、可见、后台与销毁的完整流转
做卡片组件时最常见的问题是把“系统帮你调用的生命周期”当成“自己管理App的生命周期”,结果出现各种奇怪的刷新问题。组件不是由用户主动打开的常驻进程,它由系统统筹调度,你必须在正确的时机做正确的事。
我把组件生命周期分成四个阶段来梳理:
- 创建阶段:组件实例被系统拉起,完成数据初始化注册。注意这里不要做重活,不要在创建时立刻发起网络请求,否则每次进桌面都要卡一下。
- 可见阶段:用户视线落在卡片上,这是信息窗口期。此时应保证数据尽量新鲜:可以触发一次快速更新,把最近三分钟内的状态变更同步上来。
- 后台阶段:卡片不可见但组件进程还在,系统可能保留也可能回收。这个阶段只做低频兜底更新,不做高频刷新。
- 销毁阶段:组件被移除或释放,必须把注册的资源、订阅的通道、定时器全部清理干净。漏清理的定时器是最隐蔽的吃电元凶。
这四个阶段里,后台阶段最容易踩坑。系统对后台任务调度有节能策略,不是你想唤醒就唤醒。我当时的做法是:后台更新任务全部委托给系统提供的后台任务调度能力,设定好合理的约束条件,让系统决定何时允许唤醒;前台可见时才开启自己的调度逻辑。
2.4 数据通道选型:推送优先、定时兜底
远端数据的实时更新是整条链路上最权衡的一个点。我把主流方案拉了一张对比表,逐个过了一遍:
| 方案 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 系统级推送 | 实时性好、省电、系统统一管理 | 依赖推送服务在线率、消息体有大小限制 | 状态变化不密集但需要即时通知的场景 |
| WebSocket长连接 | 全双工、延迟极低 | 常驻连接耗电、弱网容易断线重连、需要心跳保活 | 高频实时交互场景,但卡片场景性价比太低 |
| 短轮询 | 实现简单、逻辑直观 | 费电、流量浪费、服务器压力大 | 低频兜底,不适合做主通道 |
| 事件驱动+推送 | 推送在关键状态变化时触发,界面立即刷新 | 需自行处理推送丢失的补偿逻辑 | 我最终采用的主方案,配合本地事件总线 |
实测下来,根儿上的结论是:能用事件驱动就不用定时器,能靠系统推送就不自己拉。 轮询只是补救手段。我最终构架里留了一个复杂度很低的长周期轮询作为兜底,间隔至少三到五分钟,只在推送通道异常时才会提升频率。这个兜底不是为了实时,而是为了“数据永远不彻底断流”,避免卡片长期显示过期信息。
3. 开发实战:从模板渲染到事件驱动刷新
3.1 工程结构:数据层与渲染层分离
开发阶段我的工程结构分了三层:数据管理层、状态转换层、渲染层。很多新手做卡片喜欢把请求和绘制写在一个文件里,前期看着方便,等要加多尺寸适配、要加降级逻辑时直接变成一座屎山。
我把它们拆成三个模块:
- 数据管理层:负责对接本地事件通道和远端推送,产出标准化的进度状态对象。
- 状态转换层:把原始状态映射成界面文案、进度百分比、颜色变化。比如把后台返回的“picked_up”转成“已取件”,同时算出进度的百分比。
- 渲染层:只负责根据状态转换层给出的最终数据渲染界面,不关心数据来源。
这样做的好处是,我可以在完全不改渲染代码的情况下,把数据源从测试MOCK切换到真实服务,甚至可以同时接多个业务的数据源,复用同一套渲染模板。
3.2 数据绑定:状态管理让UI自动跟随
渲染层用的是ArkTS的状态管理能力。我把数据源设计成响应式数据对象,组件通过订阅该对象驱动界面更新。
关键代码长这样:
typescript复制// 活跃预览组件的数据模型
@Observed
class OrderProgress {
status: string = 'pending'; // 当前状态枚举
statusText: string = '待接单'; // 界面上显示的状态文案
progress: number = 0; // 0-100 的进度百分比
etaMinutes: number = 0; // 预计剩余分钟数
lastUpdated: number = Date.now(); // 本地最后更新时间戳
}
// 数据源单例,全局共享一份最新状态
class ProgressStore {
private static instance: ProgressStore;
private order: OrderProgress = new OrderProgress();
private listeners: Array<(value: OrderProgress) => void> = [];
static getInstance(): ProgressStore {
if (!ProgressStore.instance) {
ProgressStore.instance = new ProgressStore();
}
return ProgressStore.instance;
}
updateProgress(patch: Partial<OrderProgress>): void {
Object.assign(this.order, patch, { lastUpdated: Date.now() });
this.listeners.forEach(listener => listener(this.order));
}
subscribe(listener: (value: OrderProgress) => void): void {
this.listeners.push(listener);
}
}
组件侧这样挂订阅:
typescript复制@Entry
@Component
struct ActivePreviewCard {
@State private progress: OrderProgress = new OrderProgress();
aboutToAppear(): void {
this.progress = ProgressStore.getInstance().getSnapshot();
ProgressStore.getInstance().subscribe((value) => {
this.progress = value;
});
}
}
核心机制一句话:数据变化触发回调,回调把值赋给状态变量,框架自动重绘。 只要保证所有对数据的修改都走updateProgress这一个入口,界面的一致性就由框架兜底了。
3.3 刷新频率:事件驱动为主、轮询兜底的动态间隔
刷新频率是整个组件最有技术含量的一部分,也是性能事故的高发区。我最初天真地把刷新时间定成固定10秒,结果就是后面第四章讲的那场事故。事后我把逻辑改成了“事件驱动为主,动态间隔轮询兜底”的模式。
事件驱动不复杂:配送状态从“已接单”变成“配送中”,推送到达,数据源调用updateProgress,组件立刻重绘。这是最理想的情况,效率最高。
但推送不可靠是常态,所以还需要兜底。兜底的轮询间隔不能是常量,而要由当前数据的“活跃度”动态决定:
code复制若当前处于配送中且距离上次变更小于2分钟:
轮询间隔 = 30秒(因为状态正在快速变化,需要更频繁校准)
若当前处于配送中且距离上次变更超过2分钟:
每次轮询后间隔翻倍,上限5分钟(说明骑手暂无新事件)
若当前处于已完成或已取消:
停止轮询,只等本地数据更新(状态已经终结,无需再拉)
这套逻辑还有一个好处:它不是面向状态写的死规则,而是面向“变化的活跃度”写的自适应规则。用户看起来是“状态稳定时组件逐渐安静,状态变化时组件马上精神”。
3.4 多尺寸适配:从2x2到宽卡片的降级策略
进度类组件在不同尺寸下的信息密度要求差异很大。2x2的空间塞不下进度条加预计时间,硬塞就会视觉爆炸。我的处理原则是“按尺寸做信息降级”:
- 2x2迷你卡片:只显示当前状态文案和一个小型进度圆环,不显示具体百分比。
- 2x4标准卡片:显示状态文案、水平进度条、预计剩余时间三要素,这是主力形态。
- 宽卡片:在标准卡片基础上增加配送时间线和最后更新时间,适合锁屏或桌面横向空间充足的场景。
渲染层我用了响应式布局能力,同一个模板代码在不同尺寸下自动启用不同的信息密度配置。这里有个细节经验:别因为尺寸小就把重要信息裁掉,而是要识别出每一尺寸下用户最想知道的那个信息是什么。 迷你卡片用户想知道的是“好了没”,那状态文案和进度圆环就是核心,百分比反而是次要的。
4. 测试与调优:一次刷新过载事故的完整排查链路
4.1 事故现象:半天掉电两成
组件功能测试通过之后,我把它装进内测机准备跑一周稳定性测试。结果当天下午,那台几乎没怎么用的测试机电量掉了接近两成,明显异常。打开系统耗电排行一看,排在前面的不是屏幕也不是蜂窝信号,而是我那个组件的后台活跃。
这里先给结论的趋势性判断:卡片组件耗电异常,绝大多数不是绘制的问题,而是后台刷新的问题。 我当时还没有证据链,但这个判断帮我快速锁定了排查方向。
4.2 定位过程:从耗电排行到日志证据链
排查不是瞎猜,我按顺序做了几个动作:
第一步,看设备耗电统计。确认组件进程处于“活跃状态”,耗电占比明显高于正常预期,这直接排除了界面绘制这类瞬时功耗问题,把矛头指向后台持续行为。
第二步,抓系统日志。重点看组件对应进程的唤醒记录,结果发现每十秒左右就有一条定时唤醒日志,持续不断。此时真相已经基本清楚:我那个固定10秒的轮询代码在后台仍然生效,而且系统每次唤醒都会带起完整的数据请求流程。
第三步,看接口请求日志。后台日志显示每次唤醒都会发起一次网络请求,在信号一般的情况下,一次请求需要消耗几秒的无线模块高功率运行时间。无线模块的频繁拉起才是耗电大户,定时器本身只是引子。
完整的链路就是:定时器每10秒唤醒进程 → 进程发起网络请求 → 无线模块周期性高速运行 → 电量被持续吃掉。
4.3 策略重构:动态间隔与省电占位
定位到根因之后,我把刷新策略重构为三个动作:
第一,砍掉固定高频轮询,改成3.3节描述的动态间隔模式。状态稳定时后台轮询间隔至少三分钟,并且在组件长时间不可见或系统判定低活跃度时进一步拉长。
第二,后台请求增加“省电占位”策略。如果上一次请求刚返回过不久,且返回的数据没有产生有效状态变更,就不发起新请求。只有数据对象里的关键字段发生变化,才真的重绘界面。
第三,界面重绘本身也做了收敛。进度值如果前后差异小于两三个百分点,界面不更新,避免滚动动画和重绘带来的额外CPU消耗。
4.4 验证与兜底:连续数天测试和异常页面设计
重构完成后我又连续测了几天,耗电排行恢复到了不显眼的水平,整机续航基本和未装组件时一致。这个结果让我确信:活跃预览组件的性能问题不是不可解,关键是刷新策略必须怕“频繁”。
除了性能,我还补齐了异常兜底设计。网络断开时卡片显示“等待更新”占位,而不是留在上一次的旧数据上,避免误导用户;状态长期未更新时显示模糊的“可能延迟”提示,把判断交给用户;组件数据源收到异常报文时,必须走默认兜底文案,不能把服务器返回的原始错误码直接扔到桌面卡片上。
5. 上架生态市场的实战记录:签名、提审与常见驳回
5.1 上架前的资质、签名与版本信息
组件功能稳定后,就是上架环节。很多人觉得上架不过就是传包填表,真走一遍就知道,流程里全是之前开发时根本不会碰的细节。
上架生态市场之前必须完成开发者认证,这个绕不开,没认证连上传入口都没有。接下来是签名,生产包需要正式签名配置,签名文件一定得备份好,后续更新版本、增补能力都要用同一套签名体系。有一次我想用测试签名上传,结果提示签名不一致,浪费了半天才搞明白配置填串了。
版本信息方面,版本号不是随便写的,要跟证书配置里的版本信息对应,应用名称、包名、版本号三者要完全匹配,改任意一个都可能导致审核链路报错。
5.2 提审材料:名称描述、截图与场景说明
材料环节最容易被忽视,尤其是功能截图。我最初提交的截图是在开发工具里截的纯组件预览,灰色背景、脏数据、尺寸也不统一。审核反馈里虽然没有单独点名截图问题,但我自己复盘时明显感觉材料不专业会影响过审概率。
正确做法是:为组件准备一套“场景化截图”,演示它在真实桌面环境里的效果。我后来用测试机真实部署,拍了一张桌面上卡片展示配送进度的效果图,再加一张锁屏场景的宽卡片展示图。图片配了简短文字说明:组件怎么添加、展示哪些信息、多长时间更新一次。
应用简介和关键词也要好好写。简介不要写“高效、强大、便捷”这类空词,而是写清楚这组件是干嘛的、用户怎么用、信息更新机制是什么。关键词覆盖“桌面卡片”“实时进度”“动态预览”这类用户真实会搜的词。
5.3 审核驳回的高频问题与应对清单
审核期间我整理了一份高频问题与应对清单,这里直接共享:
| 驳回原因 | 具体表现 | 应对方式 |
|---|---|---|
| 权限与功能不符 | 申请了非必要权限,审核员质疑 | 砍掉所有非必要权限,坚持权限最小化原则 |
| 更新频率无说明 | 卡片刷新策略未在材料中交代 | 在提审材料里写明动态刷新机制和省电设计 |
| 界面信息缺失 | 卡片上没有提供停用或退订入口 | 组件设置页必须提供开关,允许用户一键停用组件 |
| 截图无场景说明 | 审核员看不懂组件实际应用场景 | 所有截图加文字标注,明确展示场景与交互方式 |
| 隐私合规存疑 | 动态数据的来源与存储未交代清楚 | 提供清晰的数据流向说明和隐私政策 |
其中权限最小化这条我体会最深。早期版本为了适配多场景,我申请了定位权限,想着以后可以按位置推送进度提醒。审核回复明确询问该权限的实际使用场景,我思考了一下,发现这个功能根本不在当前版本的规划里,于是果断移除。上架组件不是给未来做预埋,而是给当前用户交付清晰可靠的能力,多余的权限只会带来信任损耗和审核风险。
5.4 上架后的第一周:数据观察与迭代
上架只是起点,发布后的数据观察才是真正验证设计是否合理的环节。我重点看三个数据:组件安装量、使用留存率、后台耗电反馈。
第一周的数据基本符合预期:组件使用的核心场景集中在配送进度和快递物流,这说明当时聚焦这两个场景做深做透是对的。用户主动停用组件的主要原因是“桌面太挤”,而不是“更新不及时”,这间接说明动态刷新体验过关了。后台耗电相关差评目前为零,那条动态刷新的省电策略保全了我。
根据反馈,我下个迭代方向已经明确:提供可编辑的主题配色,以及让用户自定义组件展示在桌面还是锁屏的开关。这些功能不会动核心刷新链路,属于在稳定底座上做体验扩展。
回看这三周,最值得记录的不是某个炫酷的界面效果,而是“克制”本身:克制刷新频率、克制权限申请、克制功能堆砌。活跃预览组件能顺利在生态市场上架,靠的恰恰是这些不亮眼但关键的取舍。如果你也在做类似的东西,建议把刷新策略和材料合规放得比动画效果更靠前,它能帮你少走大半弯路。
