HarmonyOS APP《画伴梦工厂》开发第33篇-服务卡片开发——form_config与CreationFormAbility

第4.7篇:服务卡片开发------form_config 与 CreationFormAbility

难度 :⭐⭐⭐ 高级

前置知识 :第 1.6 篇 页面路由与生命周期

涉及源文件products/default/src/main/ets/creationformability/CreationFormAbility.etsproducts/default/form_config.json


概述

服务卡片(Service Widget)是 HarmonyOS 最具特色的系统级能力之一。它允许应用在桌面上以"卡片"形态展示关键信息,用户无需打开应用即可获取实时内容或执行快捷操作。在"画伴梦工厂"中,服务卡片用于在桌面上展示用户的创作作品概览------每次抬手看桌面,都能看到最新的或随机推荐的一幅画作标题,营造"每次都是惊喜"的体验。

服务卡片的开发涉及两个核心文件:

  1. form_config.json:卡片的声明式配置文件,定义卡片的名称、尺寸、UI 文件路径、更新策略等元信息。
  2. CreationFormAbility.ets :卡片的后端逻辑入口,继承 FormExtensionAbility,负责卡片生命周期管理、数据绑定和刷新。

本文将围绕这两个文件,从配置到代码,逐层拆解服务卡片的完整开发流程。


一、form_config.json 卡片配置详解

form_config.json 是服务卡片的"身份证",它必须放置在 products/default/ 目录下,与 src/ 同级。系统在应用安装时读取该文件,注册所有声明的服务卡片。

1.1 项目中的完整配置

json 复制代码
{
  "forms": [
    {
      "name": "creation",
      "displayName": "$string:creation_form_display_name",
      "description": "$string:creation_form_desc",
      "src": "./ets/creationcard/pages/CreationCard.ets",
      "uiSyntax": "arkts",
      "window": {
        "designWidth": 720,
        "autoDesignWidth": true
      },
      "colorMode": "auto",
      "isDynamic": true,
      "isDefault": true,
      "updateEnabled": false,
      "scheduledUpdateTime": "10:30",
      "updateDuration": 1,
      "defaultDimension": "2*4",
      "supportDimensions": ["2*4"]
    }
  ]
}

1.2 字段逐项解析

字段 说明
name "creation" 卡片唯一标识名,在代码中通过此名称引用该卡片
displayName $string:creation_form_display_name 卡片在桌面添加菜单中的显示名称,通过 $string 引用资源文件
description $string:creation_form_desc 卡片的描述文本,同样通过资源引用
src "./ets/creationcard/pages/CreationCard.ets" 卡片 UI 的 ArkTS 源文件路径,相对于 src/main/ets/ 目录
uiSyntax "arkts" UI 语法类型,"arkts" 表示使用 ArkTS 声明式语法开发卡片
window { designWidth: 720, autoDesignWidth: true } 卡片的设计尺寸基准,designWidth 为设计稿宽度(720vp),autoDesignWidth 自动适配屏幕
colorMode "auto" 卡片颜色模式,可选 "auto"(跟随系统)、"light"(浅色)、"dark"(深色)
isDynamic true 是否支持动态刷新。true 表示卡片数据可以动态更新
isDefault true 是否为默认卡片。用户在桌面添加卡片时,默认选中此规格
updateEnabled false 是否启用定时更新。设为 false 时,scheduledUpdateTimeupdateDuration 不生效
scheduledUpdateTime "10:30" 定时的更新时间(HH:mm),updateEnabledtrue 时生效
updateDuration 1 定时更新周期(单位:小时)。设为 1 表示每小时刷新一次
defaultDimension "2*4" 默认卡片尺寸,格式为 "行数*列数",系统网格中每格约 30vp
supportDimensions ["2*4"] 支持的卡片尺寸列表,可配置多个规格,如 ["1*2", "2*2", "2*4"]

1.3 关键配置设计说明

为什么不启用定时更新(updateEnabled: false)?

在"画伴梦工厂"中,卡片数据来源于用户的作品列表。当用户在应用内创作了新作品后,才需要刷新卡片。定时刷新(如每小时一次)既不必要,也浪费系统资源。因此项目采用了事件驱动 的刷新方式------在 onUpdateFormonFormEvent 生命周期回调中主动触发更新,而非依赖系统定时器。

为什么只支持 2*4 一种尺寸?

2×4 规格(约 60vp × 120vp)是桌面卡片中最适合展示单条作品信息的尺寸。它足够显示作品标题,又不会占用过多桌面空间。对于"画伴梦工厂"的使用场景------展示一条作品标题和图标------2×4 恰到好处。如需扩展,可添加 "1*2"(紧凑型)或 "4*4"(丰富信息型)等规格。

window 配置的设计意图

json 复制代码
"window": { "designWidth": 720, "autoDesignWidth": true }
  • designWidth: 720:以 720vp 宽度为设计基准,所有尺寸属性基于此缩放。
  • autoDesignWidth: true:卡片在不同屏幕密度的设备上自动缩放,确保显示效果一致。

二、FormExtensionAbility 生命周期

FormExtensionAbility 是服务卡片的后端能力入口,它继承自 ExtensionAbility,负责管理卡片从创建到销毁的完整生命周期。

2.1 生命周期全景

复制代码
用户添加卡片
     │
     ▼
onAddForm(want) → 创建卡片表单 → 返回 FormBindingData
     │
     ├──→ onCastToNormalForm(formId)  ← 临时卡片转常驻(非必须)
     │
     ├──→ onUpdateForm(formId)        ← 系统/主动触发更新
     │
     ├──→ onFormEvent(formId, msg)    ← 卡片内交互事件
     │
     ├──→ onAcquireFormState(want)    ← 查询卡片状态
     │
     └──→ onRemoveForm(formId)        ← 用户删除卡片

2.2 onAddForm:创建卡片

当用户在桌面添加"画伴梦工厂"的服务卡片时,系统调用 onAddForm 方法。这是卡片生命中最重要的起点------它负责返回卡片的初始数据。

typescript 复制代码
onAddForm(want: Want): formBindingData.FormBindingData {
  return formBindingData.createFormBindingData(this.getCardData());
}

参数说明

  • want:包含调用方信息的 Want 对象,可从 want.parameters 获取卡片参数(如卡片 ID、尺寸等)。

返回值

  • 必须返回 formBindingData.FormBindingData 类型,包含卡片 UI 需要渲染的数据。

项目中 onAddForm 的实现非常简洁------它委托给 getCardData() 方法获取最新的作品数据,然后通过 formBindingData.createFormBindingData 将其封装为可发送给卡片 UI 的数据包。

2.3 onUpdateForm:更新卡片数据

当需要刷新卡片数据时,系统或应用调用 onUpdateForm。项目中响应此回调,通过 formProvider.updateForm 主动推送新数据:

typescript 复制代码
onUpdateForm(formId: string): void {
  formProvider.updateForm(formId,
    formBindingData.createFormBindingData(this.getCardData()));
}

formProvider.updateForm@kit.FormKit 中用于更新指定卡片的 API。它接收两个参数:

  1. formId:目标卡片的唯一标识符,由系统分配。
  2. FormBindingData:新的绑定数据。

每当卡片需要刷新时(如用户创作了新作品),只需调用 onUpdateForm 即可触达所有已添加的卡片实例。

2.4 onFormEvent:响应卡片交互

当用户在卡片上执行交互操作(如点击按钮)时,卡片 UI 可以通过 postCardAction 发送事件,系统会将该事件路由到 onFormEvent

typescript 复制代码
onFormEvent(formId: string, message: string): void {
  formProvider.updateForm(formId,
    formBindingData.createFormBindingData(this.getCardData()));
}

在项目中,onFormEventonUpdateForm 的处理逻辑完全一致------都是获取最新数据并刷新卡片。这种设计使得无论以何种方式触发刷新,卡片都能展示最新的作品信息。

message 参数的典型内容

json 复制代码
{
  "action": "router",
  "bundleName": "com.example.drawing",
  "abilityName": "EntryAbility",
  "params": { "workIndex": "3" }
}

卡片 UI 通过 postCardAction 发送的事件字符串,此处可根据 message 内容实现不同的交互逻辑(如路由跳转到指定作品详情页)。

2.5 onRemoveForm:卡片被移除

当用户从桌面删除服务卡片时,系统调用此方法:

typescript 复制代码
onRemoveForm(formId: string): void {
  // 可在此清理与该卡片相关的资源
}

虽然项目中的实现为空,但在复杂场景下,可以在此处清理为该卡片分配的资源(如取消定时器、释放监听器等),避免资源泄漏。

2.6 onAcquireFormState:查询卡片状态

当系统需要了解卡片当前是否可以正常使用时,调用此方法:

typescript 复制代码
onAcquireFormState(want: Want): formInfo.FormState {
  return formInfo.FormState.READY;
}

返回值 formInfo.FormState 枚举:

枚举值 说明
FormState.READY 卡片可用
FormState.UNKNOWN 卡片状态未知

项目中始终返回 READY,表示卡片随时可用。

2.7 onCastToNormalForm:临时卡片转常驻

typescript 复制代码
onCastToNormalForm(formId: string): void {
}

当一张临时卡片(如从服务卡片预览添加的卡片)被转为常驻卡片时触发。项目中暂未涉及此场景,实现为空。


三、卡片数据绑定与 formBindingData

服务卡片的数据流遵循"后端提供数据,前端渲染 UI"的模型。formBindingData 是连接后端(FormExtensionAbility)和前端(ArkTS 卡片 UI)的桥梁。

3.1 createFormBindingData 方法

typescript 复制代码
formBindingData.createFormBindingData(this.getCardData())

createFormBindingData 接收一个普通对象,将其序列化为卡片 UI 可消费的数据结构。在项目中,这个数据对象遵循 CreationCardData 接口:

typescript 复制代码
interface CreationCardData {
  title: string;      // 作品标题
  workIndex: string;  // 作品索引号
}

这两个字段将被传递给卡片 UI 文件 CreationCard.ets,在 ArkTS 声明式 UI 中通过 @Consume@LocalStorageProp 等装饰器接收:

typescript 复制代码
// CreationCard.ets(示意)
@Entry
@Component
struct CreationCard {
  @LocalStorageProp('title') title: string = '';
  @LocalStorageProp('workIndex') string = '';

  build() {
    Column() {
      Text(this.title)
        .fontSize(16)
        .fontWeight(FontWeight.Bold)
      // ... 更多 UI 组件
    }
  }
}

3.2 数据流示意图

复制代码
CreationFormAbility.ets                CreationCard.ets
┌──────────────────────┐              ┌──────────────────┐
│ getCardData()        │              │ @LocalStorageProp │
│  → { title, workIdx }│───绑定数据──→│   title           │
│                      │              │   workIndex       │
│ createFormBindingData│              │                   │
│  → FormBindingData   │              │ build() {         │
│                      │              │   Text(title)     │
│ onAddForm → return   │              │ }                 │
└──────────────────────┘              └──────────────────┘

四、preferences 数据读取与作品数据集成

服务卡片的核心数据来源是用户已保存的视频作品。项目使用 preferences (首选项)进行本地持久化存储,CreationFormAbilityonAddFormonUpdateForm 时从 preferences 中读取最新作品。

4.1 读取最新作品

typescript 复制代码
private getLatestSavedWork(): VideoWork | undefined {
  try {
    const store = preferences.getPreferencesSync(this.context, { name: PREFERENCE_NAME });
    const rawValue = store.getSync(WORKS_KEY, '[]') as string;
    if (typeof rawValue !== 'string') {
      return undefined;
    }
    const works = JSON.parse(rawValue) as VideoWork[];
    if (works.length === 0) {
      return undefined;
    }
    return works.sort(
      (left: VideoWork, right: VideoWork): number => right.createdAt - left.createdAt
    )[0];
  } catch {
    return undefined;
  }
}

关键步骤拆解

步骤 代码 说明
1. 打开存储 preferences.getPreferencesSync(this.context, { name: 'video_work_repository' }) 获取应用首选项存储实例,name 为存储文件标识
2. 读取数据 store.getSync('works', '[]') works 键读取 JSON 字符串,默认返回空数组 '[]'
3. 类型校验 typeof rawValue !== 'string' 确保读取到的值是字符串类型,防御非预期数据
4. 反序列化 JSON.parse(rawValue) 将 JSON 字符串解析为 VideoWork 对象数组
5. 空值检查 works.length === 0 无作品时返回 undefined
6. 排序取最新 .sort((a, b) => b.createdAt - a.createdAt)[0] createdAt 降序排列,取第一条(最新)

4.2 VideoWork 数据模型

typescript 复制代码
interface VideoWork {
  title: string;     // 作品标题
  createdAt: number;  // 创建时间戳(毫秒)
}

createdAt 字段用于排序------时间戳越大的作品越新,确保卡片始终展示用户最新创作的画作。

4.3 与 WorkRepository 的集成关系

CreationFormAbility 读取的 preferences 存储与 WorkRepository(作品仓库服务)共享同一存储实例。当用户在应用内通过 WorkRepository 创建或更新作品时,数据被写入 video_work_repository 存储的 works 键下。服务卡片通过读取相同的存储键,自然获得了最新的作品数据。

这种存储层共享的设计模式,使得服务卡片与主应用之间无需额外的通信机制即可保持数据一致------它们只是从同一个"数据池"中读取数据。


五、兜底策略:随机示例作品

当用户尚未创作任何作品时(即 preferences 中 works 为空数组或不存在),getLatestSavedWork 返回 undefined。此时 getCardData 方法会退而使用预置的示例作品列表

typescript 复制代码
private getCardData(): CreationCardData {
  const latestWork = this.getLatestSavedWork();
  if (latestWork) {
    return {
      title: latestWork.title,
      workIndex: '1'
    };
  }
  const selectedWork = this.getRandomWork();
  return {
    title: selectedWork.title,
    workIndex: selectedWork.workIndex
  };
}

5.1 示例作品数据源

typescript 复制代码
const CARD_WORKS: CreationCardWork[] = [
  { title: '蜡笔森林小恐龙', workIndex: '1' },
  { title: '蜜糖色海岛寻宝', workIndex: '2' },
  { title: '彩虹城堡剧场', workIndex: '3' },
  { title: '星空飞船旅行', workIndex: '4' },
  { title: '奇妙动物伙伴', workIndex: '5' }
];

这五个作品标题充满了童趣和想象力,与"画伴梦工厂"的品牌调性高度一致。它们的作用不仅是占位,更是在新用户首次添加卡片时,展示应用的核心主题风格,吸引用户去探索和创作。

5.2 随机选择逻辑

typescript 复制代码
private getRandomWork(): CreationCardWork {
  const index = Math.floor(Math.random() * CARD_WORKS.length);
  return CARD_WORKS[index];
}

使用 Math.random() 生成 [0, 1) 间的随机数,乘以数组长度后向下取整,得到 0~4 之间的随机索引。每次添加卡片或刷新时,可能展示不同的示例作品,增加了卡片的趣味性。

5.3 策略执行流程

复制代码
getCardData()
    │
    ├── getLatestSavedWork()
    │     ├── 打开 preferences 存储
    │     ├── 读取 works 键的 JSON
    │     ├── works.length > 0 → 按 createdAt 排序 → 返回最新作品
    │     └── 异常/空 → 返回 undefined
    │
    ├── latestWork 存在 → { title: latestWork.title, workIndex: '1' }
    │
    └── latestWork 为 undefined → getRandomWork()
          ├── Math.random() × CARD_WORKS.length
          ├── 取随机索引
          └── 返回随机示例作品

这种"有用户数据就展示用户数据,没有就展示示例数据"的兜底策略,保证了卡片在任何情况下都能展示有意义的内容------用户永远不会看到空卡片。


六、卡片更新机制

6.1 更新触发方式

服务卡片的更新有四种触发方式:

触发方式 对应回调 项目中的应用
系统定时更新 onUpdateForm 未启用(updateEnabled: false
应用主动推送 formProvider.updateForm 主应用中新作品创作完成后调用
卡片交互事件 onFormEvent 用户点击卡片上的刷新按钮
卡片生命周期 onUpdateForm 系统在某些场景下自动触发

6.2 formProvider.updateForm

formProvider@kit.FormKit 中用于操作服务卡片的提供者。updateForm 方法是最核心的更新 API:

typescript 复制代码
formProvider.updateForm(formId, formBindingData.createFormBindingData(this.getCardData()));

参数详解

  • formId:系统在创建卡片时分配的字符串 ID,用于唯一标识一张卡片实例。同一应用可以添加多张同类型卡片到桌面,每张卡片拥有独立的 formId。
  • FormBindingData:新的绑定数据,替换卡片当前的 UI 数据。

6.3 更新流程时序

复制代码
主应用(Index.ets)                    CreationFormAbility              桌面卡片 UI
       │                                      │                          │
       │ 用户创作新作品                          │                          │
       │ 保存到 preferences                     │                          │
       │                                      │                          │
       │ 调用 updateForm                        │                          │
       │──────→ onUpdateForm(formId)            │                          │
       │        │                              │                          │
       │        ├── getCardData()               │                          │
       │        │   ├── getLatestSavedWork()    │                          │
       │        │   └── 获取最新作品             │                          │
       │        │                              │                          │
       │        └── formProvider.updateForm()   │                          │
       │                          │─────────────│────→ 卡片 UI 刷新        │
       │                          │             │     Text(title) 更新    │
       │                          │             │                          │

6.4 更新时机选择

项目中,卡片更新并不由系统定时器驱动,而是在以下时机触发:

  1. 卡片被添加时onAddForm)→ 返回当前最新数据
  2. 主应用中有新作品生成时 → 调用 formProvider.updateForm
  3. 卡片交互事件onFormEvent)→ 用户操作后刷新

这种"按需更新"模式相比定时更新,具有更低的功耗和更及时的数据一致性。


七、2×4 卡片尺寸与 ArkTS 卡片 UI

7.1 网格系统与卡片尺寸

HarmonyOS 的服务卡片采用网格(Grid)系统进行尺寸管理。标准网格中每个单元格的基准尺寸约为 30vp × 30vp。2×4 表示卡片宽度占 4 列、高度占 2 行:

尺寸规格 宽度 高度 适用场景
1×2 ~60vp ~30vp 紧凑型信息(如天气、日期)
2×2 ~60vp ~60vp 小型功能卡片
2×4 ~120vp ~60vp 中等信息量(如作品展示)
4×4 ~120vp ~120vp 丰富信息型(如图片墙)

项目中选用 2×4 规格,可以舒适地展示作品图标和标题,在桌面空间和信息密度之间取得良好平衡。

7.2 ArkTS 卡片 UI 语法

uiSyntax: "arkts" 表明卡片使用 ArkTS 声明式语法开发。卡片 UI 文件 CreationCard.ets 是一个标准的 @Entry @Component 结构:

typescript 复制代码
// CreationCard.ets(示意结构)
@Entry
@Component
struct CreationCard {
  @LocalStorageProp('title') title: string = '';
  @LocalStorageProp('workIndex') workIndex: string = '';

  build() {
    Column() {
      // 卡片头部图标区域
      Image($r('app.media.app_icon'))
        .width(40)
        .height(40)
        .margin({ top: 8 })

      // 作品标题
      Text(this.title)
        .fontSize(14)
        .fontWeight(FontWeight.Medium)
        .textAlign(TextAlign.Center)
        .width('100%')
        .padding({ left: 8, right: 8 })

      // 提示文字
      Text('点击查看详情')
        .fontSize(10)
        .fontColor('#888888')
    }
    .width('100%')
    .height('100%')
    .padding(12)
    .borderRadius(12)
    .backgroundColor('#FFFFFF')
  }
}

关键点解析

  • @LocalStorageProp :卡片专用的状态装饰器,用于接收 formBindingData.createFormBindingData 传递的数据。key 名称需与数据对象中的字段名一一对应。
  • @Entry:标识该组件是卡片入口,与普通页面类似。
  • 限制 :卡片 UI 中可用的组件和 API 是受限制的,不支持所有 ArkUI 组件。常见可用组件包括 TextImageColumnRowButton 等基础组件。

八、完整代码架构总览

8.1 CreationFormAbility 完整实现

typescript 复制代码
import { Want } from '@kit.AbilityKit';
import { preferences } from '@kit.ArkData';
import { formBindingData, FormExtensionAbility, formInfo, formProvider } from '@kit.FormKit';

interface CreationCardData {
  title: string;
  workIndex: string;
}

interface CreationCardWork {
  title: string;
  workIndex: string;
}

interface VideoWork {
  title: string;
  createdAt: number;
}

const PREFERENCE_NAME: string = 'video_work_repository';
const WORKS_KEY: string = 'works';
const CARD_WORKS: CreationCardWork[] = [
  { title: '蜡笔森林小恐龙', workIndex: '1' },
  { title: '蜜糖色海岛寻宝', workIndex: '2' },
  { title: '彩虹城堡剧场', workIndex: '3' },
  { title: '星空飞船旅行', workIndex: '4' },
  { title: '奇妙动物伙伴', workIndex: '5' }
];

export default class CreationFormAbility extends FormExtensionAbility {
  onAddForm(want: Want): formBindingData.FormBindingData {
    return formBindingData.createFormBindingData(this.getCardData());
  }

  private getCardData(): CreationCardData {
    const latestWork = this.getLatestSavedWork();
    if (latestWork) {
      return { title: latestWork.title, workIndex: '1' };
    }
    const selectedWork = this.getRandomWork();
    return { title: selectedWork.title, workIndex: selectedWork.workIndex };
  }

  private getLatestSavedWork(): VideoWork | undefined {
    try {
      const store = preferences.getPreferencesSync(this.context, { name: PREFERENCE_NAME });
      const rawValue = store.getSync(WORKS_KEY, '[]') as string;
      if (typeof rawValue !== 'string') { return undefined; }
      const works = JSON.parse(rawValue) as VideoWork[];
      if (works.length === 0) { return undefined; }
      return works.sort((a, b) => b.createdAt - a.createdAt)[0];
    } catch { return undefined; }
  }

  private getRandomWork(): CreationCardWork {
    const index = Math.floor(Math.random() * CARD_WORKS.length);
    return CARD_WORKS[index];
  }

  onCastToNormalForm(formId: string): void {}
  onUpdateForm(formId: string): void {
    formProvider.updateForm(formId,
      formBindingData.createFormBindingData(this.getCardData()));
  }
  onFormEvent(formId: string, message: string): void {
    formProvider.updateForm(formId,
      formBindingData.createFormBindingData(this.getCardData()));
  }
  onRemoveForm(formId: string): void {}
  onAcquireFormState(want: Want): formInfo.FormState {
    return formInfo.FormState.READY;
  }
}

8.2 模块依赖关系

复制代码
@kit.FormKit (formBindingData, FormExtensionAbility, formInfo, formProvider)
    │
    ├── formBindingData.createFormBindingData()  ← 封装卡片数据
    ├── FormExtensionAbility                      ← 卡片能力基类
    ├── formInfo.FormState                        ← 卡片状态枚举
    └── formProvider.updateForm()                 ← 推送卡片更新

@kit.AbilityKit (Want)                            ← 卡片参数传递
@kit.ArkData (preferences)                        ← 作品数据持久化

8.3 与主应用的数据流关系

复制代码
主应用 WorkRepository                CreationFormAbility              桌面卡片
┌─────────────────────┐             ┌──────────────────────┐        ┌────────────┐
│  preferences 存储    │             │                      │        │            │
│  ┌───────────────┐  │  读取同一    │  getLatestSavedWork()│        │ 卡片 UI    │
│  │ video_work_   │←│───存储─────│  → 排序取最新作品      │──数据──→│ 标题展示   │
│  │ repository    │  │             │                      │        │            │
│  │  └ works ──┐  │  │             │  或 getRandomWork()   │        │            │
│  │            │  │  │             │  → 随机预置作品       │        │            │
│  │  JSON 数组  │  │  │             │                      │        │            │
│  └───────────────┘  │             └──────────────────────┘        └────────────┘
│                     │
│ 写入新作品 → flush  │
└─────────────────────┘

九、服务卡片的注册与配置

要让 CreationFormAbility 生效,还需要在 module.json5 中注册 ExtensionAbility。这是连接 form_config.jsonCreationFormAbility.ets 的关键环节:

json 复制代码
{
  "module": {
    "extensionAbilities": [
      {
        "name": "CreationFormAbility",
        "srcEntry": "./ets/creationformability/CreationFormAbility.ets",
        "description": "服务卡片能力",
        "type": "form",
        "metadata": [
          {
            "name": "ohos.extension.form",
            "resource": "$profile:form_config"
          }
        ]
      }
    ]
  }
}

配置要点

  • type : "form" 表示这是一个服务卡片 ExtensionAbility。
  • srcEntry : 指向 CreationFormAbility.ets 的路径。
  • metadata : 通过 "ohos.extension.form" 名称和 $profile:form_config 资源引用,将 form_config.json 与当前 ExtensionAbility 关联起来。
  • form_config.json 文件通常放置在 resources/base/profile/ 目录下(项目中的路径为 products/default/form_config.json,构建时会被复制到 profile 资源目录)。

十、最佳实践与注意事项

10.1 数据量控制

服务卡片可携带的数据量有限,FormBindingData 的序列化大小不宜过大。建议仅传输核心展示字段(如 title),避免将完整的作品 JSON 数据(含图片 URI、故事文本等)全部塞入卡片。

10.2 异常处理

getLatestSavedWork 中的 try-catch 保护至关重要------preferences 操作可能因存储损坏、权限不足等原因抛出异常。不加保护的话,卡片添加时可能直接崩溃。

10.3 资源释放

onRemoveForm 虽然项目中未使用,但在实际开发中应养成习惯:若在卡片生命周期中注册了监听器或开启了定时器,务必在 onRemoveForm 中对称释放。

10.4 卡片 UI 限制

ArkTS 卡片 UI 相比普通页面有较多限制:

  • 不支持所有 ArkUI 组件(仅支持基础组件)
  • 不支持 @State@Link 等装饰器,数据只能通过 @LocalStorageProp / @LocalStorageLink 接收
  • 不支持网络请求、文件读写等耗时操作
  • 交互能力有限(主要通过 postCardActiononFormEvent 通信)

10.5 formId 的管理

每张添加到桌面的卡片都有一个唯一的 formId,应用需要妥善管理这些 ID。当需要全量刷新所有卡片时,可遍历 formId 列表逐一调用 updateForm


总结

本文通过"画伴梦工厂"的服务卡片实现,完整呈现了 HarmonyOS 服务卡片的开发全流程:

知识点 实现方式
form_config.json 配置 定义卡片名称、尺寸(2×4)、UI 路径、更新策略等元信息
FormExtensionAbility 生命周期 onAddForm 创建 → onUpdateForm 更新 → onRemoveForm 销毁
卡片数据绑定 formBindingData.createFormBindingData 封装数据,@LocalStorageProp 在 UI 中消费
preferences 集成 从 video_work_repository 存储读取最新作品,按 createdAt 排序
兜底策略 无用户数据时退而使用随机预置作品(5 个童趣标题)
卡片更新 onUpdateForm + formProvider.updateForm 组合,事件驱动而非定时驱动
ArkTS 卡片 UI @Entry @Component 声明式卡片,受限组件集
注册配置 module.json5 中 extensionAbilities 配置 type: "form",metadata 关联 form_config

服务卡片的设计哲学是"轻量、聚焦、实时"------用最轻量的数据展示最聚焦的信息,在用户需要时提供最新的内容。这种"桌面即服务"的体验,正是 HarmonyOS 全场景智慧生态的生动体现。


参考源码

本文所有代码均来自项目文件:

  • products/default/src/main/ets/creationformability/CreationFormAbility.ets --- 服务卡片 ExtensionAbility 的完整实现,包含生命周期回调、preferences 数据读取、随机作品兜底等核心方法
  • products/default/form_config.json --- 服务卡片的 JSON 配置文件,定义所有卡片规格和元数据