1. 为什么我盯上了轻量化交互:HarmonyOS 6交互场景的变化
先说个真实场景。之前我们团队在做一款工具类应用,用户反馈最多的不是功能缺失,而是"看一条数据太费劲"。比如用户想看一眼今天的待办完成率,原本的路径是:点开应用图标 -> 等待冷启动 -> 进入首页 -> 切换Tab -> 找到统计卡片 -> 才看到数字。一个动作拆成五步,每一步都消耗用户的耐心。
在HarmonyOS 6的生态里,这个矛盾会变得更尖锐。系统的服务流转、多设备协同越来越深,用户对"信息触达"的预期已经从"打开应用慢慢看"变成了"抬手就能瞄一眼"。如果应用还停留在"必须完整启动才能拿到核心信息"的阶段,那留不住用户是大概率事件。
这就是"轻量化交互"要解决的问题:把用户最关注的信息和服务提炼成轻量入口,缩短交互路径,减少完整页面跳转和重复渲染。我这次在HarmonyOS 6上做的改造,核心就围绕两条路线展开。
- 方案A:服务卡片(Form)重构,把核心信息搬到桌面和应用外。用户不用打开应用,在桌面、负一屏或服务中心就能完成查看和基础操作。
- 方案B:应用内组件级轻量化,用组件复用和状态共享替代完整页面跳转。用户在应用内部操作时,减少无意义的Activity/Page切换和重复布局计算,让每一步操作响应更快、过渡更流畅。
这篇先讲方案A的完整改造流程,方案B的架构和落地细节放到下篇。不是故意吊胃口,而是两个方案背后各自的坑都够单开一篇写清楚,混在一起反而讲不透。
先说清楚:这篇文章不打算从环境搭建开始教,假设你已经能跑通一个基础的HarmonyOS工程,也理解Stage模型的基础概念。我重点讲的是"改造"——从一根筋的传统页面交互,到轻量化触达的思考路径和实操细节。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 两种方案的选型拆解:先想清楚再动手
改造之前最重要的一步不是写代码,而是搞清楚你的应用到底适合哪种轻量化方案。很多团队上来就做卡片,做完发现用户根本不看桌面;或者一心想做应用内优化,结果核心数据却藏得太深。问题出在选型阶段就错了。
2.1 方案A的适用边界:什么功能适合做服务卡片
服务卡片的本质是"免启动、即时可见、可操作"。它适合的信息有三个特征:
- 信息天然短平快。比如天气、待办数量、快递状态、股票行情、设备状态。用户不需要上下文铺垫,看一眼数字就能决策。
- 高频且需要主动提醒的。如果信息是你推给用户的,而不是用户来找你的,那就天然适合卡片。比如"今天是项目截止日,还有3个任务未完成"。
- 操作链路可以收敛在一步以内。也就是说,卡片上允许的最大操作是"点一下完成一个简单动作",比如勾选待办、开启设备、播放一首歌。凡是需要多个参数确认的交互,都不应该硬塞进卡片。
我们这次改造挑了两个入口做卡片:一个是项目进度面板(2x4尺寸,展示今日完成率、未完成任务数、下一节点时间),另一个是快捷开始(4x4尺寸,把最近使用的三个项目直接摆出来,点击直达对应详情页)。
选这两个功能的原因很直接:用户高频使用,且核心信息不需要额外解释。你自己评估的时候,可以拿一张纸把潜在功能列出来,每个功能打三个勾:是否高频、是否短信息、是否单步操作。三个全中的才进卡片候选池,否则老老实实留在应用内。
2.2 方案B的设计初衷:应用内交互的"轻"不是减功能
方案B要解决的是另一个问题:应用已经打开了,但用户在里面完成一个任务的路径还是太长、响应还是太重。HarmonyOS 6的ArkUI支持组件级复用、@Reusable装饰器、懒加载LazyForEach,这些能力如果不用,那你的应用内交互就还停留在"每个页面独立加载、每次跳转全量渲染"的旧思路。
方案B我们计划做三件事:第一,把高频页面里的稳定区块抽成可复用组件,配合@Reusable避免滚动场景下的反复创建销毁;第二,用AppStorage和LocalStorage做页面间状态共享,去掉多余的跨页传参和重复请求;第三,针对详情页与列表页的跳转,用共享元素转场(transition效果)让用户在视觉上感知"连续",而不是"新开了一个页面"。
这两个方案不是互斥的,卡片承担"应用外触达",组件化承担"应用内提速"。在HarmonyOS 6的生态里,两者还会通过卡片点击事件和UIAbility拉起串联起来——比如用户在卡片上点击一个项目,直接拉起应用并定位到对应详情页。这个串联逻辑我后面会详细展开。
2.3 为什么没考虑其他方案:一次对比排雷
调研阶段我也看了其他几种轻量化路子,做了一圈对比,结论很明确:
| 候选方案 | 优点 | 缺点 | 本次是否采用 |
|---|---|---|---|
| 服务卡片(Form) | 触达路径最短、系统级入口、生态统一 | 布局受限、运行环境精简、刷新需要策略 | 采用(方案A) |
| 悬浮窗/胶囊 | 应用内随时可达、不占桌面 | 权限限制严格、容易干扰用户、审核风险 | 放弃 |
| 原子化服务/免安装 | 体验连贯、按需加载 | 改造成本高、业务闭环不适合 | 放弃 |
| 通知(Notification) | 实现简单、用户熟悉 | 信息被动、点按后仍需拉起应用 | 作为辅助,不替代卡片 |
| 应用内组件化+状态共享 | 治本、提升整体流畅度 | 需要重构现有页面结构、收益需要数据验证 | 采用(方案B,下篇) |
提示:卡片和通知的边界很容易混淆。卡片的优势是"持续可见",通知的优势是"即时打扰"。如果你要的是提醒一下就完事,通知更轻;如果你要的是"用户随时回看、随时操作",卡片才是正确答案。
3. 方案A落地第一步:Stage模型下的卡片工程改造
选型做完,接下来就是动手。这篇的实操主线是方案A——服务卡片。在HarmonyOS 6上,卡片开发依赖Stage模型,整个工程结构和API调用和传统的FA模型完全不同,如果你还在用旧的Widget开发思路,第一步就会卡住。
3.1 从卡片模板创建工程:不是一个普通Page
很多初学者最大的误区是把服务卡片当成一个普通页面来写——在MainAbility里加一个组件就完事。但HarmonyOS 6的卡片本质上是由系统桌面进程渲染的独立UI,它不跑在你的应用进程里,而是通过FormExtensionAbility这个扩展能力来提供数据和生命周期管理。
在DevEco Studio里创建卡片的正确姿势是:新建工程时选择Empty Ability模板,然后右键工程目录 -> New -> Service Widget,选择卡片模板。DevEco会自动帮你生成一套完整的卡片代码骨架,包括:
EntryFormAbility.ets:这是卡片的扩展能力类,所有生命周期回调都在这里。form_config.json:卡片配置文件,声明卡片的尺寸、名称、是否可更新等元数据。- 卡片UI描述的ets文件:用ArkTS声明式描述卡片长什么样。
- 关联的
module.json5配置:把FormExtensionAbility注册进来。
我自己吃过的亏:最初是手动在module.json5里写extensionAbilities配置,结果漏了srcEntry路径,导致运行的时候系统一直找不到卡片能力。有模板不用非要手搓,弯路走得不冤。建议你们新建卡片时直接让IDE生成模板,然后再基于模板去改。
3.2 配置文件的字段逐个说清
form_config.json是卡片能否被系统正确识别的关键。HarmonyOS 6对字段校验很严格,少一个、写错一个类型都会直接导致卡片加载失败。这是我认为最值得逐行讲清楚的地方:
json复制{
"forms": [
{
"name": "ProgressForm",
"displayName": "$string:progress_form_name",
"description": "$string:progress_form_description",
"src": "./ets/forms/ProgressForm.ets",
"uiSyntax": "arkts",
"window": {
"designWidth": 720,
"autoDesignWidth": true
},
"isDefault": true,
"updateEnabled": true,
"scheduledUpdateTime": "10:30",
"updateDuration": 6,
"defaultDimension": "2*4",
"supportDimensions": ["2*2", "2*4", "4*4"]
}
]
}
几个容易翻车的字段:
uiSyntax必须写成arkts,这是HarmonyOS 6声明式卡片UI的标识,写错会被当成旧语法解析而失败。window.designWidth是卡片设计基准宽度,推荐用720,autoDesignWidth置true,让系统按实际卡片尺寸自动缩放。这里如果配错,你在预览器里看到的效果和真机实装效果会差很多。updateEnabled和updateDuration控制定时刷新。updateDuration单位是小时(对,不是分钟),最小1小时,最大一般不建议超过24。如果你期望的是分钟级刷新,那不能用定时刷新,得走我第4章讲的数据推送更新。supportDimensions里声明的尺寸,必须和ets文件里对应的布局都实现,否则用户切换到未实现的尺寸时会白屏。
注意:DevEco的卡片预览器给出的是一个估算效果,不同桌面(华为桌面、第三方桌面)渲染卡片时可能存在边距差异。真机调试时一定要在多个桌面环境下都看一眼布局有没有被裁切。
3.3 卡片尺寸的响应式适配边界
HarmonyOS 6的卡片尺寸体系是网格化的,以2x2为基准单位,常见的有2x2、2x4、4x4。系统会根据用户添加到桌面的位置和尺寸,决定卡片实际占用的网格数,并选择对应的布局来渲染。
说实话,同时维护三套布局的成本不低。本次改造我们最终只保留了2x4和4x4两个尺寸,2x2用于信息密度极低的场景。原因很实际:2x2的空间放不下我们的项目进度核心数据,硬塞一个"完成率57%"的半截信息,对用户来说是噪音。如果你判断某个尺寸放不下核心信息,宁可不支持,也不要做一个视觉拥挤的低质量卡片。
每个尺寸对应一个独立的ets入口文件,在form_config.json的src字段分别声明。也可以用同一个文件,在内部用formDimension参数做分支渲染,但不推荐——不同尺寸下,不只是字号不一样,组件排布逻辑甚至交互方式都可能不同,写在一起没过多久你就会想重构。
4. 方案A的核心链路:FormExtensionAbility的数据绑定与刷新
工程骨架跑通之后,真正的核心问题来了:卡片是怎么拿到数据的?它怎么更新?用户点了卡片怎么跳回应用?这三个问题对应卡片开发的三条关键链路:数据绑定、刷新机制、点击事件路由。我按实际开发顺序一条条说。
4.1 卡片生命周期与FormBindingData的数据传递
卡片UI的运行环境是精简的,它不能直接访问网络、不能直接读数据库(在严格模式下甚至不建议做复杂计算)。它所能展示的数据,全部由EntryFormAbility在生命周期回调中准备好,通过formBindingData.createFormBindingData()打包成FormBindingData对象,交还给系统渲染。
生命周期里最关键的回调就两个:
onAddForm(want):用户把卡片添加到桌面的瞬间回调。这是首次数据绑定的时机。onUpdateForm(formId):系统判定卡片需要更新时回调(定时刷新、拉起时机或主动推更时触发)。
我这里用一个简化版示例说明数据绑定的核心逻辑:
typescript复制// EntryFormAbility.ets
import formBindingData from '@ohos.app.form.formBindingData';
import formInfo from '@ohos.app.form.formInfo';
import formProvider from '@ohos.app.form.formProvider';
export default class EntryFormAbility extends FormExtensionAbility {
// 用户把卡片添加到桌面时回调
onAddForm(want) {
const formId = want.parameters[formInfo.FormParam.IDENTITY_KEY];
const data = this.buildCardData(formId);
return formBindingData.createFormBindingData(data);
}
// 系统要求更新时回调
onUpdateForm(formId) {
const data = this.buildCardData(formId);
formProvider.updateForm(formId, formBindingData.createFormBindingData(data))
.then(() => {
console.info(`Form ${formId} updated successfully`);
})
.catch((err) => {
console.error(`Update form failed, code: ${err.code}, message: ${err.message}`);
});
}
// 统一组装卡片数据
buildCardData(formId) {
// 实际项目中这里从业务仓库拉取数据
const progressData = this.getTodayProgress();
return {
taskProgress: progressData.percent, // 数值类型
taskSummary: progressData.summary, // 字符串
nextDeadline: progressData.nextDeadline,
projectId: progressData.projectId // 用于点击跳转
};
}
}
卡片ets文件里拿到这些字段后,直接用{{}}模板语法绑定到组件上。这里有个细节:createFormBindingData的入参是扁平化的键值结构,不能传嵌套对象。好多人在这一步尝试传一个{ data: { percent: 57 } },结果卡片UI上一直渲染不出来。不是语法错了,是数据绑定层不接受这种嵌套,必须拍平成第一层key。
4.2 定时刷新与数据推送更新:两种触发方式怎么选
卡片刷新是轻量化交互里最容易被忽视、也最容易踩性能坑的环节。HarmonyOS 6给了两种刷新的手段:
- 定时刷新:配置
updateEnabled和updateDuration,系统到点自动调onUpdateForm。优点是省心,缺点是颗粒度粗(小时级)、且系统会聚合多个应用的刷新请求来省电,实际触发时间不精确。 - 主动推送更新:应用进程内通过
formProvider.updateForm(formId, formBindingData)实时下发新数据。精确到秒级,但要求应用进程是活着的,且每次更新都是一次IPC跨进程通信,频繁调用会明显增加系统负载。
我们最终选择的是以主动推送更新为主的策略。原因:项目进度数据跟用户的操作强相关——用户勾选了一个任务,卡片上的完成率应该马上变,而不是等到下一个整点。每次勾选动作后调用一次updateForm,频率极低(一天也就几十次),完全在合理范围内。
另外还有一个策略问题要提醒:卡片刷新失败后的静默降级。如果updateForm因为系统负载或进程被杀而失败,卡面上会保留旧数据。这时候用户看到的可能是一个已经过时的完成率,比没有卡片更影响信任感。我做的兜底是:每次刷新的同时,顺带启动一个定时任务,每隔两小时用定时刷新机制强制同步一次,双保险,保证卡片数据最多只会落后两小时。
4.3 卡片点击跳转的两种事件:router和message
卡片上的交互不能像普通页面那样自由绑定任意手势,系统只开放了有限的卡片事件,最常用的是这两种:
- router事件:通过
postCardAction触发,拉起应用内的指定UIAbility页面。 - message事件:通过
postCardAction触发,只向应用进程发送一条message,由EntryFormAbility的onFormMessage回调接收处理,不拉起任何页面。
从使用经验来看,凡是"跳过去看详情"的场景,用router;凡是"后台做个小动作"的场景,用message。比如我们卡片上的"快捷开始"按钮,每个项目按钮点击时要携带不同的projectId到详情页,就在卡片事件里这样写:
typescript复制// 卡片ets文件内
Button('查看项目详情')
.onClick(() => {
postCardAction(this, {
action: 'router',
abilityName: 'EntryAbility',
params: {
targetPage: 'ProjectDetailPage',
projectId: this.projectId
}
});
});
// 快捷操作:标记今日任务完成(不需要跳转)
Button('标记完成')
.onClick(() => {
postCardAction(this, {
action: 'message',
params: {
actionType: 'completeTask',
taskId: this.taskId
}
});
});
这里有个务必记住的坑:router事件的目标是UIAbility,不是直接跳到某个Page。很多新手以为params里写个页面名就自动跳转了,实际上UIAbility收到拉起请求后,需要自己在onNewWant里解析参数并执行页面路由。我见过不止一个项目卡在"卡片点击没反应",排查半天发现是UIAbility侧根本没有处理want.parameters里的路由逻辑。
在UIAbility入口加一段路由分发就行,本质是:
typescript复制// EntryAbility.ets 的onNewWant中处理
onNewWant(want, launchParam) {
const params = want.parameters;
if (params && params.targetPage) {
this.context.getApplicationContext();
// 解析targetPage,执行router.pushUrl跳转
}
}
这套"卡片点击 -> 拉起UIAbility -> 路由到指定Page"的链路,是串联桌面轻量入口和应用内体验的关键,值得在项目初期就做好设计。
5. 性能和兼容性:卡片不能做成"看起来轻,实际很重"
服务卡片的UI运行在一个受限环境里,能用的组件集比完整ArkUI少很多。我体会最深的一句总结是:想在卡片里塞越多东西,最后会发现能用的东西越少。在设计阶段就要把"克制"刻在脑子里。
5.1 卡片能用什么组件:一套精简的组件集
HarmonyOS 6的卡片UI支持基础的文本、按钮、图片、进度条和简单的容器布局,但有一批常见组件在卡片里是不可用的,比如List(列表)、Scroll(滚动)、Canvas(画布)、Video(视频)、Web(网页)。这意味着卡片上的信息密度不能超过一屏,不能有可滚动区域,一切布局必须静态化。
我们最初设计4x4卡片时放了六个区块:进度环、任务列表、下一截止时间、快捷按钮、团队头像、状态标签。到了真机一看,任务列表渲染一多就溢出,而Scroll根本用不了。最终砍掉了任务列表,改为"只显示总数+前三项重点任务",信息密度下来了,反而视觉更清爽。
提示:卡片UI的调试不像普通页面那么方便。
console.info可以看,但无法在卡片UI文件里使用断点调试。遇到渲染问题,建议先在卡片UI里写死一段测试数据,能渲染出来再接真实数据源。这样能快速二分定位问题是出在渲染层还是数据层。
5.2 首帧响应的优化:卡片不能有"白屏等待"
用户往桌面放一张卡片,如果第一眼看到的是白屏或长时间加载中的占位,那这张卡片的信任感直接归零。卡片的加载体验比普通应用内页面更严格——用户已经在了,没有启动页可以缓冲。
我的优化手段分两层:
- 启动数据缓存到本地:
onAddForm触发时,先从本地缓存读取上次的数据快照直接返回给卡片,确保首帧有内容。后台再异步从服务端拉最新数据,拉到了再推送更新。这样用户体验上的一致性会好很多。 - 图片走网络要慎重:卡片上的图片如果必须走网络加载,建议优先使用小尺寸缩略图和系统缓存。因为卡片加载图片时,没有应用内那么强的缓存管理机制,大图会拖慢首帧渲染,甚至会因为内存压力被系统直接回收。
5.3 多设备适配:卡片不是手机的专属
HarmonyOS 6的卡片机制天然跨设备:手机、平板、折叠屏、甚至部分轻量设备上都可以挂卡片。这带来了一个适配问题——不同设备的桌面网格密度不同,同一张卡片在不同设备上的实际物理尺寸不一样。
如果你的卡片是从手机为核心设计的,建议通过form_config.json里的supportDimensions按设备能力做差异化配置。比如平板可以额外支持4x4的大卡片,手机则只提供2x4。这个不是一次性配置能搞定的,我在实际项目中的做法是在配置里全量声明,代码里针对大尺寸布局做一次额外的密度适配。
6. 实测中的意外:卡片跨端同步的坑
所有功能开发完,我原本觉得这就齐活了,直到真机压测环节连续踩了两个坑,每个都花费了大半天才定位清楚。这章的内容值回整篇票价。
6.1 深拷贝问题:同一份数据,卡片上是旧值
第一个坑出现在双设备协同场景:手机和平板都挂了同一张卡片,手机端完成一个操作后更新了卡片数据,但平板端卡片纹丝不动。排查日志时发现updateForm在手机端调用是成功的,返回码正常,但平板端就是没刷新。
问题根源是跨设备刷新走的是分布式数据同步链路,FormBindingData在跨端传输时只做了浅拷贝,嵌套对象和多层结构在同步过程中数据丢失。我们项目里卡片数据整体是一个对象,里面还嵌了一层Map结构,同步时Map直接变成空对象,卡片UI上所有依赖Map的字段全部取了默认值。
修复方案是:在组装FormBindingData前,把所有参数拍平、做一次JSON序列化再反序列化的深拷贝处理。虽然有点土,但稳定可靠。卡片数据本来就不应该有过深的嵌套结构,如果需要同步,数据结构越平越好。
6.2 卡片刷新频率的分级管理
第二个坑是性能问题:我们一开始对每张卡片都设置了updateDuration: 1(每小时刷新一次),以为这是最合理的频率。结果压测时发现,系统内存占用率明显上升,卡片的onUpdateForm回调变得不稳定,偶尔出现延迟几十秒才刷新的情况。
后来翻系统文档才知道,HarmonyOS对卡片刷新有分组刷新机制:系统会把不同应用的卡片刷新请求聚合成批,避免频繁唤醒。updateDuration设置得越密,你的卡片就越容易被系统调度进高频刷新组,反而导致每次刷新的间隔不可控。
我的修正策略是:
| 卡片类型 | 刷新策略 | 场景说明 |
|---|---|---|
| 进度面板 | 主动推送为主,定时刷新兜底(2小时) | 用户操作触发更新,低频兜底 |
| 快捷开始 | 免刷新,只在onAddForm时绑定 | 项目列表变化频率极低 |
| 离线状态卡 | 定时刷新(6小时)+ 主动推更 | 状态变化可控,推更即时性要求低 |
把刷新频率分级管理之后,系统负载降下来了,卡片的刷新及时性反而更稳定了。不要对所有卡片一视同仁地设同一个刷新周期,根据数据变化频率给每张卡片单独定制更新策略,是卡片性能调优最有效的一步。
6.3 卡片的负面场景:用户删掉卡片后你的代码还在跑吗
最后一个容易被忽略的点:卡片被用户删除后,应用进程还在,但卡片的数据流要立刻断掉。否则会出现一种尴尬情况:用户早就把卡片删了,你的应用还在后台周期性地为这张不存在的卡片拉数据、组装数据、调updateForm,白耗流量和电量。
正确做法是在onRemoveForm(formId)回调里做清理:取消这张卡片的定时任务、移除对应的数据订阅、释放相关引用。这样既省资源,也避免后续可能产生的log异常。
typescript复制onRemoveForm(formId) {
// 取消该卡片对应的定时同步任务
this.cancelScheduleSync(formId);
// 从订阅列表中移除该卡片
this.unsubscribeFormData(formId);
console.info(`Form ${formId} removed, clean up done`);
}
7. 上篇的思考沉淀与下篇预告
方案A的服务卡片改造,到此算是完整落地了。复盘整个改造过程,最核心的认知是:轻量化交互的本质不是做减法,而是做取舍。你要清楚哪些信息值得被搬到卡片上,哪些操作值得被精简成一步——这一步的判断力,比任何API熟练度都重要。技术上,FormExtensionAbility的生命周期管理、FormBindingData的数据拍平、点击事件的双通道路由,这三板斧掌握之后,绝大多数的卡片场景都不在话下。
但也得承认,卡片改造的价值天花板在于"应用外触达"。用户一旦进入应用内部,轻量化交互的重心就转移到了方案B:组件复用、状态共享、渲染优化。下篇我会从我们实际重构首页和详情页的案例出发,讲清楚怎么用@Reusable装饰器做组件复用,怎么用AppStorage和LocalStorage把跨页面的通信次数砍掉一大半,以及共享元素转场如何改变用户对页面跳转的体感。
如果你正在HarmonyOS 6上做类似改造,我的建议是:先用一张纸写出你的核心用户旅程,找到最痛的那一步,再决定用卡片还是组件化来解决它。工具永远是第二位的,搞清楚改造的目标,才是第一位的。
