HarmonyOS Form Kit 实战给卡片工具接入桌面卡片桌面卡片是 HarmonyOS 应用很有辨识度的能力。这个项目里主应用负责创建和管理卡片桌面 Form 负责展示当前选中的卡片摘要。实现上分成四块module.json5注册、form_config.json配置、EntryFormAbility生命周期、ArkTS Form 页面。这篇按真实工程链路拆不只讲“怎么把 Form 配出来”。Project028 后面做过几轮桌面卡片修复从默认尺寸、系统添加面板、主题同步、风格同步到桌面 Form 主动刷新都说明 Form Kit 不是一个孤立页面而是主应用状态、系统 Form 生命周期和桌面展示之间的桥接层。先拆清楚桌面 Form 的目标在这个项目里桌面卡片不是把 App 首页搬到桌面也不是在 Form 里重新实现一套业务逻辑。它只做三件事展示当前卡片工具的摘要比如总卡片数、收藏数和当前选中的卡片。展示用户当前选择的视觉风格让桌面卡片跟应用内风格保持一致。点击桌面卡片时打开主应用让编辑、归档、备份等复杂操作仍回到主应用完成。这个边界很重要。桌面 Form 的运行环境和主应用页面不同如果把查询、筛选、编辑、备份都塞进 Form 页面后续会很难维护。Project028 的做法是主应用维护长期状态DesktopFormService把状态压成扁平数据DesktopCardForm.ets只负责渲染。主应用页面 - AppDataService 保存卡片和桌面选择 - DesktopFormService 生成 FormBindingData - EntryFormAbility 响应系统生命周期 - DesktopCardForm 渲染桌面卡片module.json5 注册 FormExtensionAbility先在module.json5里声明type: form的扩展能力{ name: EntryFormAbility, srcEntry: ./ets/entryformability/EntryFormAbility.ets, label: $string:EntryFormAbility_label, description: $string:EntryFormAbility_desc, type: form, metadata: [ { name: ohos.extension.form, resource: $profile:form_config } ] }这一步不要漏。只写 ArkTS Form 页面但不注册 extensionAbility系统不会把它当桌面卡片能力。form_config.json 配置卡片尺寸和刷新form_config.json指向真正的卡片页面{ forms: [ { name: desktop_card, src: ./ets/widget/pages/DesktopCardForm.ets, uiSyntax: arkts, window: { designWidth: 720, autoDesignWidth: true }, colorMode: auto, isDynamic: false, isDefault: true, updateEnabled: true, updateDuration: 2, defaultDimension: 2*4, supportDimensions: [2*4, 2*2], formVisibleNotify: true, supportDeviceTypes: [phone] } ] }当前项目支持2*4和2*2默认使用2*4。这个默认值不是随便选的2*4横向空间更适合展示摘要数字、主卡片标题、分类和更新时间如果默认2*2内容容易挤压长标题和副标题会频繁截断。{ window: { designWidth: 720, autoDesignWidth: true }, colorMode: auto, defaultDimension: 2*4, supportDimensions: [2*4, 2*2] }这里有两个容易忽略的配置window.designWidth让 ArkTS Form 页面按稳定画布设计不要让不同设备上的宽度推算完全失控。colorMode: auto跟随系统深浅色Form 页面里使用项目 token 才能稳定适配暗色模式。如果后续继续扩展尺寸不要只改supportDimensions。还要同步检查DesktopCardForm.ets里数字区、标题区、背景图裁剪和底部文案的空间否则系统能添加卡片但桌面上的展示不一定合格。详情页如何触发系统添加面板用户在卡片详情页点“添加到桌面卡片”时项目不是自己模拟桌面行为而是调用系统的formProvider.openFormManager()private handleDesktopAction(): void { if (this.isTemplate || this.card.id.length 0) { return; } const updated: boolean appDataService.setDesktopCard(this.card.id); if (!updated) { return; } this.desktopCardId this.card.id; refreshDesktopForms(); this.openDesktopFormManager(); }这里的顺序不能乱。先把当前卡片写入AppDataService再调用refreshDesktopForms()最后打开系统添加面板。这样即使用户之前已经添加过桌面卡片已有 Form 也能立即刷新到最新选择如果用户是第一次添加系统面板添加完成后onAddForm()也能拿到同一份当前状态。打开系统面板时项目显式传了卡片名、模块名和尺寸const want: Want { bundleName: DESKTOP_FORM_BUNDLE_NAME, abilityName: DESKTOP_FORM_ABILITY_NAME, parameters: { ohos.extra.param.key.form_dimension: DESKTOP_FORM_DIMENSION_2_4, ohos.extra.param.key.form_name: DESKTOP_FORM_NAME, ohos.extra.param.key.module_name: DESKTOP_FORM_MODULE_NAME } }; formProvider.openFormManager(want);这能保证入口和form_config.json的desktop_card对齐。实际项目里如果form_name和配置里的name不一致常见现象不是编译失败而是系统添加面板里找不到目标卡片或者添加后不是预期尺寸。失败兜底也要留着try { formProvider.openFormManager(want); } catch (_error) { this.getUIContext().getPromptAction().showToast({ message: 无法打开桌面卡片添加页请在桌面长按添加服务卡片 }); }这个兜底看起来很小但对真机验证很实用。不同设备和系统版本上系统添加页可能因为桌面环境、权限或启动状态失败用户至少要知道还有“桌面长按添加服务卡片”这条路径。EntryFormAbility添加、更新、移除都要初始化数据Form 生命周期在EntryFormAbility.etsexport default class EntryFormAbility extends FormExtensionAbility { onAddForm(want: Want): formBindingData.FormBindingData { this.ensureDataReady(); this.registerFormId(want); return createDesktopCardFormBindingData(); } onUpdateForm(formId: string): void { this.ensureDataReady(); formProvider.updateForm(formId, createDesktopCardFormBindingData()).catch(() { }); } onRemoveForm(formId: string): void { this.ensureDataReady(); appDataService.unregisterDesktopFormId(formId); } }桌面卡片不一定和主应用页面同时运行所以每个生命周期里都先ensureDataReady()。这能保证 Form 读取本地状态前AppDataService已经初始化。记录 formId用于主动刷新添加 Form 时项目会保存系统传入的 formIdprivate registerFormId(want: Want): void { const parameters want.parameters; if (!parameters) { return; } const formId: string parameters[formInfo.FormParam.IDENTITY_KEY] as string; if (formId formId.length 0) { appDataService.registerDesktopFormId(formId); } }有了 formId主应用里设置桌面卡片后就能主动刷新export function refreshDesktopForms(): void { const formIds: string[] appDataService.getDesktopFormIds(); if (formIds.length 0) { return; } const bindingData createDesktopCardFormBindingData(); formIds.forEach((formId: string) { formProvider.updateForm(formId, bindingData).catch(() { }); }); }formId保存逻辑在AppDataService中关键点是去重和持久化registerDesktopFormId(formId: string): void { const normalized: string formId.trim(); if (normalized.length 0 || this.state.desktopFormIds.indexOf(normalized) 0) { return; } this.state.desktopFormIds.push(normalized); this.persistState(); } unregisterDesktopFormId(formId: string): void { const normalized: string formId.trim(); if (normalized.length 0 || this.state.desktopFormIds.indexOf(normalized) 0) { return; } this.state.desktopFormIds this.state.desktopFormIds.filter((item: string) item ! normalized); this.persistState(); }这里不能只把formId放在页面变量里。桌面卡片可能在主应用退出后仍存在也可能由系统触发刷新如果formId不持久化下一次应用启动后就不知道该刷新哪些桌面卡片。另外setDesktopCard()本身也不是只改一个字段它还会记录活动日志setDesktopCard(cardId: string): boolean { const card: CardRecordModel | undefined this.findCardById(cardId); if (!card || !card.active) { return false; } if (this.state.desktopCardId card.id) { return true; } this.state.desktopCardId card.id; this.recordActivity(update, 设置桌面卡片「 card.title 」, 1, new Date()); this.persistState(); return true; }这样统计页能看到“设置桌面卡片”这类操作桌面 Form 也不会和主应用状态脱节。DesktopFormService把业务状态转成 Form 数据Form 页面不直接读服务层而是使用 binding dataexport interface DesktopCardFormData { title: string; subtitle: string; value: string; footer: string; badge: string; primaryTitle: string; primarySubtitle: string; primaryValue: string; primaryCategory: string; updatedAt: string; }生成数据时优先读取当前指定的桌面卡片export function createDesktopCardFormData(): DesktopCardFormData { const summary appDataService.getHomeSummaryCard(); const primary appDataService.getDesktopFormCard(); return { title: summary.title, subtitle: summary.subtitle, value: summary.value ? summary.value : 0 张, footer: summary.footer ? summary.footer : 打开应用管理桌面卡片, badge: summary.badge ? summary.badge : 同步, primaryTitle: primary ? primary.title : 新建桌面卡片, primarySubtitle: primary ? primary.subtitle : 打开应用创建第一张卡片, primaryValue: primary ? primary.value : 0 张, primaryCategory: primary ? appDataService.getCategoryLabel(primary.categoryId) : 空白, updatedAt: formatMinute(new Date()) }; }这样桌面 Form 展示的是业务摘要不是页面临时状态。当前实现还把主题和风格一起压进 Form 数据const selectedThemeId: string appDataService.getSelectedTheme().id; const selectedStyleId: string appDataService.getSelectedStyle().id; const selectedStyleTitle: string appDataService.getSelectedStyle().title; return { selectedThemeId: selectedThemeId, selectedStyleId: selectedStyleId, themeImageKey: imageKeyForStyle(selectedStyleId), footer: createDesktopFooter(summary.footer, selectedStyleTitle) };这样做有两个好处。第一桌面 Form 不需要知道风格对象的完整结构只需要themeImageKey。第二底部文案能显示当前风格例如风格 海盐蓝 / 打开应用管理桌面卡片用户能确认桌面卡片跟应用内选择是一致的。主题和风格变化如何同步到桌面Project028 后面修过一个典型问题应用内换了主题或风格桌面服务卡片没有明显变化。最后的处理方式不是让 Form 自己轮询而是在主题和风格写入后主动调用refreshDesktopForms()。风格页保存时private applyStyle(styleId: string): void { appDataService.setSelectedStyle(styleId); refreshDesktopForms(); this.refreshData(); }主题页保存时private applyTheme(themeId: string): void { appDataService.setSelectedTheme(themeId); refreshDesktopForms(); this.refreshData(); }路由带入主题参数时也会同步private applyRouteTheme(): void { const params: RouteParams (router.getParams() ?? {}) as RouteParams; if (params.themeId params.themeId.length 0) { appDataService.setSelectedTheme(params.themeId); refreshDesktopForms(); } }这说明桌面 Form 的刷新触发点要放在业务状态变化处而不是只放在 Form 生命周期里。onUpdateForm()能响应系统刷新但用户在 App 内的主动选择也要立即推给桌面。ArkTS Form 页面使用 LocalStoragePropDesktopCardForm.ets用LocalStorage接收绑定数据let formStorage new LocalStorage(); Entry(formStorage) Component struct DesktopCardForm { LocalStorageProp(title) title: string 我的桌面卡片; LocalStorageProp(subtitle) subtitle: string 当前使用 0 张收藏 0 张; LocalStorageProp(value) value: string 0 张; LocalStorageProp(updatedAt) updatedAt: string --:--; }这里要注意使用LocalStorageProp时Entry需要传入LocalStorage。否则构建或运行时可能出现绑定不同步的问题。FormLink 负责点击打开主应用桌面卡片整体包在FormLink中FormLink({ action: router, abilityName: EntryAbility, params: { source: desktop-card-form } }) { // card UI }用户点击桌面卡片后进入主应用。后续如果要跳到具体详情页可以继续扩展 params再由EntryAbility或首页读取来源参数处理。验证建议Form Kit 接入后验证module.json5是否存在type: form扩展能力。form_config.json的src是否指向 ArkTS Form 页面。添加桌面卡片时onAddForm()是否返回数据。主应用设置桌面卡片后refreshDesktopForms()是否更新 Form。移除桌面卡片后formId 是否从本地状态移除。点击 Form 是否能打开主应用。基础链路小结这个项目的桌面 Form 没有绕过业务层而是复用AppDataService和DesktopFormService。主应用负责状态Form 负责展示EntryFormAbility负责生命周期和系统桥接。接入 Form Kit 时最容易漏的是三处同步维护module.json5、form_config.json、ArkTS Form 页面。只改其中一处系统很可能不会按预期识别或刷新桌面卡片。Form 数据流的关键边界桌面卡片接入不能只贴form_config.json。Project028 的桌面卡片链路包括三层应用页面选择卡片服务层把当前状态压成FormBindingDataForm 页面通过LocalStorageProp消费绑定数据。只有把这三层拆清楚才能解释为什么页面里改主题、改样式后桌面卡片也能刷新。DesktopFormService.ets的createDesktopCardFormData()是核心转换点。它从appDataService.getDesktopFormCard()获取当前被设置为桌面卡片的业务卡再把标题、副标题、分类、主题图片 key 等字段压缩成 Form 可读的结构。桌面 Form 不应该自己访问复杂业务模型因为 Form 运行环境和主应用不同保持数据扁平更稳。interface DesktopCardFormData { title: string; subtitle: string; value: string; footer: string; badge: string; primaryTitle: string; primarySubtitle: string; primaryValue: string; primaryCategory: string; selectedThemeId: string; selectedStyleId: string; themeImageKey: string; updatedAt: string; }Form 页面使用LocalStorageProp接收绑定字段。这样写的好处是清晰Form 只关心title、subtitle、themeImageKey这些稳定字段复杂的卡片编辑、统计、备份逻辑都留在主应用。这条经验很关键桌面 Form 是展示终端不是业务中枢。LocalStorageProp(title) title: string 我的桌面卡片; LocalStorageProp(themeImageKey) themeImageKey: string CardImageKeys.styleSeaTile; Image(cardImageResource(this.themeImageKey)) .width(100%) .height(100%)还要说明刷新策略。refreshDesktopForms()遍历本地保存的formId对每一个执行formProvider.updateForm(formId, bindingData)。这里 catch 为空并不是忽略错误而是避免某个失效 Form 阻塞主应用交互如果要做生产级监控可以在 catch 中记录日志或清理失效 id。2*4 卡片布局要优先保证信息密度DesktopCardForm.ets当前按2*4做主展示。背景图走themeImageKey并使用ImageFit.CoverImage(cardImageResource(this.themeImageKey)) .width(100%) .height(100%) .objectFit(ImageFit.Cover) .opacity(0.26)内容层分成四块顶部徽标和更新时间、标题副标题、数字摘要和当前卡片、底部风格提示。这样的结构能在横向卡片中同时展示“状态”和“入口”。Row({ space: 10 }) { Column() { Text(this.value) .fontSize(28) .fontWeight(FontWeight.Bold) .fontColor($r(app.color.brand_primary)) .maxLines(1) .textOverflow({ overflow: TextOverflow.Ellipsis }) } .width(74) .height(58) Column({ space: 3 }) { Text(this.primaryTitle) .fontSize(16) .fontWeight(FontWeight.Bold) .maxLines(1) .textOverflow({ overflow: TextOverflow.Ellipsis }) Text(this.primarySubtitle) .fontSize(12) .maxLines(1) .textOverflow({ overflow: TextOverflow.Ellipsis }) } .layoutWeight(1) .constraintSize({ minWidth: 0 }) }constraintSize({ minWidth: 0 })这类细节很容易被忽略。没有它时右侧标题区在窄宽度下可能不按预期收缩最终表现为文本顶开布局或省略号不生效。桌面 Form 的空间比 App 页面更受限制长标题、长副标题一定要用maxLines和TextOverflow.Ellipsis控住。常见问题复盘接入 Form Kit 时常见问题基本集中在配置、生命周期和状态同步三个方向。第一类是“系统识别不到卡片”。通常是module.json5没有声明type: form或者metadata.resource没有指向$profile:form_config。这类问题先查配置不要先怀疑 ArkUI 页面。{ name: ohos.extension.form, resource: $profile:form_config }第二类是“添加成功但内容不对”。这往往是onAddForm()没有先初始化数据或者DesktopFormService读到的是默认空状态。Project028 在onAddForm()、onUpdateForm()、onRemoveForm()都调用ensureDataReady()就是为了让 Form 生命周期进入时服务层先准备好。第三类是“App 内改了桌面不变”。这时要看业务动作后有没有调用refreshDesktopForms()以及本地有没有保存有效formId。只依赖系统周期刷新会让用户感觉桌面卡片不同步。第四类是“封面或背景显示不对”。桌面 Form 只拿themeImageKey最终资源解析在CardImages.ets。如果风格 ID 和图片 key 映射漏了Form 页面不会知道业务含义只会显示默认图或旧图。验证清单这条链路的验收不能只看构建通过至少要按下面几步过一遍检查module.json5中EntryFormAbility是否存在type是否为form。检查form_config.json的name、src、defaultDimension、supportDimensions是否与详情页入口参数一致。运行构建确认 Form 页面和资源引用能通过编译。在真机或模拟器从详情页触发“添加到桌面卡片”确认系统添加面板打开的是2*4卡片。添加后查看桌面卡片标题、摘要、当前卡片、更新时间和底部风格文案是否正确。在应用内切换风格或主题确认已有桌面卡片背景和底部文案跟着变化。移除桌面卡片后确认onRemoveForm()能清理本地formId后续刷新不会一直推送到失效目标。构建命令可以按项目基线执行.\hvigorw.bat assembleHap --mode module -p moduleentrydefault真机侧先确认设备连接hdc list targets -v如果要看运行期日志可以围绕EntryFormAbility、formProvider.openFormManager和updateForm关键字过滤。核心不是日志越多越好而是确认添加、更新、移除三个生命周期都能被触发。落地检查清单是否讲清楚主应用、服务层、Form 页面三层分工。是否说明为什么 Form 数据要扁平化。是否解释themeImageKey与CardImages.ets的资源映射。是否提醒updateForm可能失败不要让失败影响主应用主流程。是否覆盖真实路径DesktopFormService.ets、DesktopCardForm.ets、form_config.json。
HarmonyOS Form Kit 实战:给卡片工具接入桌面卡片
HarmonyOS Form Kit 实战给卡片工具接入桌面卡片桌面卡片是 HarmonyOS 应用很有辨识度的能力。这个项目里主应用负责创建和管理卡片桌面 Form 负责展示当前选中的卡片摘要。实现上分成四块module.json5注册、form_config.json配置、EntryFormAbility生命周期、ArkTS Form 页面。这篇按真实工程链路拆不只讲“怎么把 Form 配出来”。Project028 后面做过几轮桌面卡片修复从默认尺寸、系统添加面板、主题同步、风格同步到桌面 Form 主动刷新都说明 Form Kit 不是一个孤立页面而是主应用状态、系统 Form 生命周期和桌面展示之间的桥接层。先拆清楚桌面 Form 的目标在这个项目里桌面卡片不是把 App 首页搬到桌面也不是在 Form 里重新实现一套业务逻辑。它只做三件事展示当前卡片工具的摘要比如总卡片数、收藏数和当前选中的卡片。展示用户当前选择的视觉风格让桌面卡片跟应用内风格保持一致。点击桌面卡片时打开主应用让编辑、归档、备份等复杂操作仍回到主应用完成。这个边界很重要。桌面 Form 的运行环境和主应用页面不同如果把查询、筛选、编辑、备份都塞进 Form 页面后续会很难维护。Project028 的做法是主应用维护长期状态DesktopFormService把状态压成扁平数据DesktopCardForm.ets只负责渲染。主应用页面 - AppDataService 保存卡片和桌面选择 - DesktopFormService 生成 FormBindingData - EntryFormAbility 响应系统生命周期 - DesktopCardForm 渲染桌面卡片module.json5 注册 FormExtensionAbility先在module.json5里声明type: form的扩展能力{ name: EntryFormAbility, srcEntry: ./ets/entryformability/EntryFormAbility.ets, label: $string:EntryFormAbility_label, description: $string:EntryFormAbility_desc, type: form, metadata: [ { name: ohos.extension.form, resource: $profile:form_config } ] }这一步不要漏。只写 ArkTS Form 页面但不注册 extensionAbility系统不会把它当桌面卡片能力。form_config.json 配置卡片尺寸和刷新form_config.json指向真正的卡片页面{ forms: [ { name: desktop_card, src: ./ets/widget/pages/DesktopCardForm.ets, uiSyntax: arkts, window: { designWidth: 720, autoDesignWidth: true }, colorMode: auto, isDynamic: false, isDefault: true, updateEnabled: true, updateDuration: 2, defaultDimension: 2*4, supportDimensions: [2*4, 2*2], formVisibleNotify: true, supportDeviceTypes: [phone] } ] }当前项目支持2*4和2*2默认使用2*4。这个默认值不是随便选的2*4横向空间更适合展示摘要数字、主卡片标题、分类和更新时间如果默认2*2内容容易挤压长标题和副标题会频繁截断。{ window: { designWidth: 720, autoDesignWidth: true }, colorMode: auto, defaultDimension: 2*4, supportDimensions: [2*4, 2*2] }这里有两个容易忽略的配置window.designWidth让 ArkTS Form 页面按稳定画布设计不要让不同设备上的宽度推算完全失控。colorMode: auto跟随系统深浅色Form 页面里使用项目 token 才能稳定适配暗色模式。如果后续继续扩展尺寸不要只改supportDimensions。还要同步检查DesktopCardForm.ets里数字区、标题区、背景图裁剪和底部文案的空间否则系统能添加卡片但桌面上的展示不一定合格。详情页如何触发系统添加面板用户在卡片详情页点“添加到桌面卡片”时项目不是自己模拟桌面行为而是调用系统的formProvider.openFormManager()private handleDesktopAction(): void { if (this.isTemplate || this.card.id.length 0) { return; } const updated: boolean appDataService.setDesktopCard(this.card.id); if (!updated) { return; } this.desktopCardId this.card.id; refreshDesktopForms(); this.openDesktopFormManager(); }这里的顺序不能乱。先把当前卡片写入AppDataService再调用refreshDesktopForms()最后打开系统添加面板。这样即使用户之前已经添加过桌面卡片已有 Form 也能立即刷新到最新选择如果用户是第一次添加系统面板添加完成后onAddForm()也能拿到同一份当前状态。打开系统面板时项目显式传了卡片名、模块名和尺寸const want: Want { bundleName: DESKTOP_FORM_BUNDLE_NAME, abilityName: DESKTOP_FORM_ABILITY_NAME, parameters: { ohos.extra.param.key.form_dimension: DESKTOP_FORM_DIMENSION_2_4, ohos.extra.param.key.form_name: DESKTOP_FORM_NAME, ohos.extra.param.key.module_name: DESKTOP_FORM_MODULE_NAME } }; formProvider.openFormManager(want);这能保证入口和form_config.json的desktop_card对齐。实际项目里如果form_name和配置里的name不一致常见现象不是编译失败而是系统添加面板里找不到目标卡片或者添加后不是预期尺寸。失败兜底也要留着try { formProvider.openFormManager(want); } catch (_error) { this.getUIContext().getPromptAction().showToast({ message: 无法打开桌面卡片添加页请在桌面长按添加服务卡片 }); }这个兜底看起来很小但对真机验证很实用。不同设备和系统版本上系统添加页可能因为桌面环境、权限或启动状态失败用户至少要知道还有“桌面长按添加服务卡片”这条路径。EntryFormAbility添加、更新、移除都要初始化数据Form 生命周期在EntryFormAbility.etsexport default class EntryFormAbility extends FormExtensionAbility { onAddForm(want: Want): formBindingData.FormBindingData { this.ensureDataReady(); this.registerFormId(want); return createDesktopCardFormBindingData(); } onUpdateForm(formId: string): void { this.ensureDataReady(); formProvider.updateForm(formId, createDesktopCardFormBindingData()).catch(() { }); } onRemoveForm(formId: string): void { this.ensureDataReady(); appDataService.unregisterDesktopFormId(formId); } }桌面卡片不一定和主应用页面同时运行所以每个生命周期里都先ensureDataReady()。这能保证 Form 读取本地状态前AppDataService已经初始化。记录 formId用于主动刷新添加 Form 时项目会保存系统传入的 formIdprivate registerFormId(want: Want): void { const parameters want.parameters; if (!parameters) { return; } const formId: string parameters[formInfo.FormParam.IDENTITY_KEY] as string; if (formId formId.length 0) { appDataService.registerDesktopFormId(formId); } }有了 formId主应用里设置桌面卡片后就能主动刷新export function refreshDesktopForms(): void { const formIds: string[] appDataService.getDesktopFormIds(); if (formIds.length 0) { return; } const bindingData createDesktopCardFormBindingData(); formIds.forEach((formId: string) { formProvider.updateForm(formId, bindingData).catch(() { }); }); }formId保存逻辑在AppDataService中关键点是去重和持久化registerDesktopFormId(formId: string): void { const normalized: string formId.trim(); if (normalized.length 0 || this.state.desktopFormIds.indexOf(normalized) 0) { return; } this.state.desktopFormIds.push(normalized); this.persistState(); } unregisterDesktopFormId(formId: string): void { const normalized: string formId.trim(); if (normalized.length 0 || this.state.desktopFormIds.indexOf(normalized) 0) { return; } this.state.desktopFormIds this.state.desktopFormIds.filter((item: string) item ! normalized); this.persistState(); }这里不能只把formId放在页面变量里。桌面卡片可能在主应用退出后仍存在也可能由系统触发刷新如果formId不持久化下一次应用启动后就不知道该刷新哪些桌面卡片。另外setDesktopCard()本身也不是只改一个字段它还会记录活动日志setDesktopCard(cardId: string): boolean { const card: CardRecordModel | undefined this.findCardById(cardId); if (!card || !card.active) { return false; } if (this.state.desktopCardId card.id) { return true; } this.state.desktopCardId card.id; this.recordActivity(update, 设置桌面卡片「 card.title 」, 1, new Date()); this.persistState(); return true; }这样统计页能看到“设置桌面卡片”这类操作桌面 Form 也不会和主应用状态脱节。DesktopFormService把业务状态转成 Form 数据Form 页面不直接读服务层而是使用 binding dataexport interface DesktopCardFormData { title: string; subtitle: string; value: string; footer: string; badge: string; primaryTitle: string; primarySubtitle: string; primaryValue: string; primaryCategory: string; updatedAt: string; }生成数据时优先读取当前指定的桌面卡片export function createDesktopCardFormData(): DesktopCardFormData { const summary appDataService.getHomeSummaryCard(); const primary appDataService.getDesktopFormCard(); return { title: summary.title, subtitle: summary.subtitle, value: summary.value ? summary.value : 0 张, footer: summary.footer ? summary.footer : 打开应用管理桌面卡片, badge: summary.badge ? summary.badge : 同步, primaryTitle: primary ? primary.title : 新建桌面卡片, primarySubtitle: primary ? primary.subtitle : 打开应用创建第一张卡片, primaryValue: primary ? primary.value : 0 张, primaryCategory: primary ? appDataService.getCategoryLabel(primary.categoryId) : 空白, updatedAt: formatMinute(new Date()) }; }这样桌面 Form 展示的是业务摘要不是页面临时状态。当前实现还把主题和风格一起压进 Form 数据const selectedThemeId: string appDataService.getSelectedTheme().id; const selectedStyleId: string appDataService.getSelectedStyle().id; const selectedStyleTitle: string appDataService.getSelectedStyle().title; return { selectedThemeId: selectedThemeId, selectedStyleId: selectedStyleId, themeImageKey: imageKeyForStyle(selectedStyleId), footer: createDesktopFooter(summary.footer, selectedStyleTitle) };这样做有两个好处。第一桌面 Form 不需要知道风格对象的完整结构只需要themeImageKey。第二底部文案能显示当前风格例如风格 海盐蓝 / 打开应用管理桌面卡片用户能确认桌面卡片跟应用内选择是一致的。主题和风格变化如何同步到桌面Project028 后面修过一个典型问题应用内换了主题或风格桌面服务卡片没有明显变化。最后的处理方式不是让 Form 自己轮询而是在主题和风格写入后主动调用refreshDesktopForms()。风格页保存时private applyStyle(styleId: string): void { appDataService.setSelectedStyle(styleId); refreshDesktopForms(); this.refreshData(); }主题页保存时private applyTheme(themeId: string): void { appDataService.setSelectedTheme(themeId); refreshDesktopForms(); this.refreshData(); }路由带入主题参数时也会同步private applyRouteTheme(): void { const params: RouteParams (router.getParams() ?? {}) as RouteParams; if (params.themeId params.themeId.length 0) { appDataService.setSelectedTheme(params.themeId); refreshDesktopForms(); } }这说明桌面 Form 的刷新触发点要放在业务状态变化处而不是只放在 Form 生命周期里。onUpdateForm()能响应系统刷新但用户在 App 内的主动选择也要立即推给桌面。ArkTS Form 页面使用 LocalStoragePropDesktopCardForm.ets用LocalStorage接收绑定数据let formStorage new LocalStorage(); Entry(formStorage) Component struct DesktopCardForm { LocalStorageProp(title) title: string 我的桌面卡片; LocalStorageProp(subtitle) subtitle: string 当前使用 0 张收藏 0 张; LocalStorageProp(value) value: string 0 张; LocalStorageProp(updatedAt) updatedAt: string --:--; }这里要注意使用LocalStorageProp时Entry需要传入LocalStorage。否则构建或运行时可能出现绑定不同步的问题。FormLink 负责点击打开主应用桌面卡片整体包在FormLink中FormLink({ action: router, abilityName: EntryAbility, params: { source: desktop-card-form } }) { // card UI }用户点击桌面卡片后进入主应用。后续如果要跳到具体详情页可以继续扩展 params再由EntryAbility或首页读取来源参数处理。验证建议Form Kit 接入后验证module.json5是否存在type: form扩展能力。form_config.json的src是否指向 ArkTS Form 页面。添加桌面卡片时onAddForm()是否返回数据。主应用设置桌面卡片后refreshDesktopForms()是否更新 Form。移除桌面卡片后formId 是否从本地状态移除。点击 Form 是否能打开主应用。基础链路小结这个项目的桌面 Form 没有绕过业务层而是复用AppDataService和DesktopFormService。主应用负责状态Form 负责展示EntryFormAbility负责生命周期和系统桥接。接入 Form Kit 时最容易漏的是三处同步维护module.json5、form_config.json、ArkTS Form 页面。只改其中一处系统很可能不会按预期识别或刷新桌面卡片。Form 数据流的关键边界桌面卡片接入不能只贴form_config.json。Project028 的桌面卡片链路包括三层应用页面选择卡片服务层把当前状态压成FormBindingDataForm 页面通过LocalStorageProp消费绑定数据。只有把这三层拆清楚才能解释为什么页面里改主题、改样式后桌面卡片也能刷新。DesktopFormService.ets的createDesktopCardFormData()是核心转换点。它从appDataService.getDesktopFormCard()获取当前被设置为桌面卡片的业务卡再把标题、副标题、分类、主题图片 key 等字段压缩成 Form 可读的结构。桌面 Form 不应该自己访问复杂业务模型因为 Form 运行环境和主应用不同保持数据扁平更稳。interface DesktopCardFormData { title: string; subtitle: string; value: string; footer: string; badge: string; primaryTitle: string; primarySubtitle: string; primaryValue: string; primaryCategory: string; selectedThemeId: string; selectedStyleId: string; themeImageKey: string; updatedAt: string; }Form 页面使用LocalStorageProp接收绑定字段。这样写的好处是清晰Form 只关心title、subtitle、themeImageKey这些稳定字段复杂的卡片编辑、统计、备份逻辑都留在主应用。这条经验很关键桌面 Form 是展示终端不是业务中枢。LocalStorageProp(title) title: string 我的桌面卡片; LocalStorageProp(themeImageKey) themeImageKey: string CardImageKeys.styleSeaTile; Image(cardImageResource(this.themeImageKey)) .width(100%) .height(100%)还要说明刷新策略。refreshDesktopForms()遍历本地保存的formId对每一个执行formProvider.updateForm(formId, bindingData)。这里 catch 为空并不是忽略错误而是避免某个失效 Form 阻塞主应用交互如果要做生产级监控可以在 catch 中记录日志或清理失效 id。2*4 卡片布局要优先保证信息密度DesktopCardForm.ets当前按2*4做主展示。背景图走themeImageKey并使用ImageFit.CoverImage(cardImageResource(this.themeImageKey)) .width(100%) .height(100%) .objectFit(ImageFit.Cover) .opacity(0.26)内容层分成四块顶部徽标和更新时间、标题副标题、数字摘要和当前卡片、底部风格提示。这样的结构能在横向卡片中同时展示“状态”和“入口”。Row({ space: 10 }) { Column() { Text(this.value) .fontSize(28) .fontWeight(FontWeight.Bold) .fontColor($r(app.color.brand_primary)) .maxLines(1) .textOverflow({ overflow: TextOverflow.Ellipsis }) } .width(74) .height(58) Column({ space: 3 }) { Text(this.primaryTitle) .fontSize(16) .fontWeight(FontWeight.Bold) .maxLines(1) .textOverflow({ overflow: TextOverflow.Ellipsis }) Text(this.primarySubtitle) .fontSize(12) .maxLines(1) .textOverflow({ overflow: TextOverflow.Ellipsis }) } .layoutWeight(1) .constraintSize({ minWidth: 0 }) }constraintSize({ minWidth: 0 })这类细节很容易被忽略。没有它时右侧标题区在窄宽度下可能不按预期收缩最终表现为文本顶开布局或省略号不生效。桌面 Form 的空间比 App 页面更受限制长标题、长副标题一定要用maxLines和TextOverflow.Ellipsis控住。常见问题复盘接入 Form Kit 时常见问题基本集中在配置、生命周期和状态同步三个方向。第一类是“系统识别不到卡片”。通常是module.json5没有声明type: form或者metadata.resource没有指向$profile:form_config。这类问题先查配置不要先怀疑 ArkUI 页面。{ name: ohos.extension.form, resource: $profile:form_config }第二类是“添加成功但内容不对”。这往往是onAddForm()没有先初始化数据或者DesktopFormService读到的是默认空状态。Project028 在onAddForm()、onUpdateForm()、onRemoveForm()都调用ensureDataReady()就是为了让 Form 生命周期进入时服务层先准备好。第三类是“App 内改了桌面不变”。这时要看业务动作后有没有调用refreshDesktopForms()以及本地有没有保存有效formId。只依赖系统周期刷新会让用户感觉桌面卡片不同步。第四类是“封面或背景显示不对”。桌面 Form 只拿themeImageKey最终资源解析在CardImages.ets。如果风格 ID 和图片 key 映射漏了Form 页面不会知道业务含义只会显示默认图或旧图。验证清单这条链路的验收不能只看构建通过至少要按下面几步过一遍检查module.json5中EntryFormAbility是否存在type是否为form。检查form_config.json的name、src、defaultDimension、supportDimensions是否与详情页入口参数一致。运行构建确认 Form 页面和资源引用能通过编译。在真机或模拟器从详情页触发“添加到桌面卡片”确认系统添加面板打开的是2*4卡片。添加后查看桌面卡片标题、摘要、当前卡片、更新时间和底部风格文案是否正确。在应用内切换风格或主题确认已有桌面卡片背景和底部文案跟着变化。移除桌面卡片后确认onRemoveForm()能清理本地formId后续刷新不会一直推送到失效目标。构建命令可以按项目基线执行.\hvigorw.bat assembleHap --mode module -p moduleentrydefault真机侧先确认设备连接hdc list targets -v如果要看运行期日志可以围绕EntryFormAbility、formProvider.openFormManager和updateForm关键字过滤。核心不是日志越多越好而是确认添加、更新、移除三个生命周期都能被触发。落地检查清单是否讲清楚主应用、服务层、Form 页面三层分工。是否说明为什么 Form 数据要扁平化。是否解释themeImageKey与CardImages.ets的资源映射。是否提醒updateForm可能失败不要让失败影响主应用主流程。是否覆盖真实路径DesktopFormService.ets、DesktopCardForm.ets、form_config.json。