服务卡片开发指导

卡片概述

服务卡片(以下简称“卡片”)是一种界面展示形式,可以将应用的重要信息或操作前置到卡片,以达到服务直达、减少体验层级的目的。

卡片常用于嵌入到其他应用(当前只支持系统应用)中作为其界面的一部分显示,并支持拉起页面、发送消息等基础的交互功能。

卡片的基本概念:

  • 卡片使用方:显示卡片内容的宿主应用,控制卡片在宿主中展示的位置。

  • 卡片管理服务:用于管理系统中所添加卡片的常驻代理服务,包括卡片对象的管理与使用,以及卡片周期性刷新等。

  • 卡片提供方:提供卡片显示内容原子化服务,控制卡片的显示内容、控件布局以及控件点击事件。

运作机制

卡片框架的运作机制如图1所示。

图1 卡片框架运作机制(FA模型)
form-extension

卡片使用方包含以下模块:

  • 卡片使用:包含卡片的创建、删除、请求更新等操作。

  • 通信适配层:由OpenHarmony SDK提供,负责与卡片管理服务通信,用于将卡片的相关操作到卡片管理服务。

卡片管理服务包含以下模块:

  • 周期性刷新:在卡片添加后,根据卡片的刷新策略启动定时任务周期性触发卡片的刷新。

  • 卡片缓存管理:在卡片添加到卡片管理服务后,对卡片的视图信息进行缓存,以便下次获取卡片时可以直接返回缓存数据,降低时延。

  • 卡片生命周期管理:对于卡片切换到后台或者被遮挡时,暂停卡片的刷新;以及卡片的升级/卸载场景下对卡片数据的更新和清理。

  • 卡片使用方对象管理:对卡片使用方的RPC对象进行管理,用于使用方请求进行校验以及对卡片更新后的回调处理。

  • 通信适配层:负责与卡片使用方和提供方进行RPC通信。

卡片提供方包含以下模块:

  • 卡片服务:由卡片提供方开发者实现,开发者实现生命周期处理创建卡片、更新卡片以及删除卡片等请求,提供相应的卡片服务。

  • 卡片提供方实例管理模块:由卡片提供方开发者实现,负责对卡片管理服务分配的卡片实例进行持久化管理。

  • 通信适配层:由OpenHarmony SDK提供,负责与卡片管理服务通信,用于将卡片的更新数据主动推送到卡片管理服务。

说明: 实际开发时只需要作为卡片提供方进行卡片内容的开发,卡片使用方和卡片管理服务由系统自动处理。

接口说明

FormAbility生命周期接口如下:

接口名 描述
onCreate(want: Want): formBindingData.FormBindingData 卡片提供方接收创建卡片的通知接口。
onCastToNormal(formId: string): void 卡片提供方接收临时卡片转常态卡片的通知接口
onUpdate(formId: string): void 卡片提供方接收更新卡片的通知接口。
onVisibilityChange(newStatus: { [key: string]: number }): void 卡片提供方接收修改可见性的通知接口。
onEvent(formId: string, message: string): void 卡片提供方接收处理卡片事件的通知接口。
onDestroy(formId: string): void 卡片提供方接收销毁卡片的通知接口。
onAcquireFormState?(want: Want): formInfo.FormState 卡片提供方接收查询卡片状态的通知接口。
onShare?(formId: string): {[key: string]: any} 卡片提供方接收卡片分享的通知接口。

FormProvider类有如下API接口,具体的API介绍详见接口文档

接口名 描述
setFormNextRefreshTime(formId: string, minute: number, callback: AsyncCallback<void>): void; 设置指定卡片的下一次更新时间。
setFormNextRefreshTime(formId: string, minute: number): Promise<void>; 设置指定卡片的下一次更新时间,以promise方式返回。
updateForm(formId: string, formBindingData: FormBindingData, callback: AsyncCallback<void>): void; 更新指定的卡片。
updateForm(formId: string, formBindingData: FormBindingData): Promise<void>; 更新指定的卡片,以promise方式返回。

formBindingData类有如下API接口,具体的API介绍详见接口文档

接口名 描述
createFormBindingData(obj?: Object | string): FormBindingData 创建一个FormBindingData对象。

开发步骤

FA卡片开发,即基于FA模型的卡片提供方开发,主要涉及如下关键步骤:

实现卡片生命周期接口

创建FA模型的卡片,需实现卡片的生命周期接口。先参考IDE开发服务卡片指南生成服务卡片模板。

  1. 在form.ts中,导入相关模块

    import formBindingData from '@ohos.app.form.formBindingData';
    import formInfo from '@ohos.app.form.formInfo';
    import formProvider from '@ohos.app.form.formProvider';
    import dataStorage from '@ohos.data.storage';
    
  2. 在form.ts中,实现卡片生命周期接口

    export default {
      onCreate(want) {
        console.info('FormAbility onCreate');
        // 使用方创建卡片时触发,提供方需要返回卡片数据绑定类
        let obj = {
          "title": "titleOnCreate",
          "detail": "detailOnCreate"
        };
        let formData = formBindingData.createFormBindingData(obj);
        return formData;
      },
      onCastToNormal(formId) {
        // 使用方将临时卡片转换为常态卡片触发,提供方需要做相应的处理
        console.info('FormAbility onCastToNormal');
      },
      onUpdate(formId) {
        // 若卡片支持定时更新/定点更新/卡片使用方主动请求更新功能,则提供方需要重写该方法以支持数据更新
        console.info('FormAbility onUpdate');
        let obj = {
          "title": "titleOnUpdate",
          "detail": "detailOnUpdate"
        };
        let formData = formBindingData.createFormBindingData(obj);
        formProvider.updateForm(formId, formData).catch((error) => {
          console.info('FormAbility updateForm, error:' + JSON.stringify(error));
        });
      },
      onVisibilityChange(newStatus) {
        // 使用方发起可见或者不可见通知触发,提供方需要做相应的处理,仅系统应用生效
        console.info('FormAbility onVisibilityChange');
      },
      onEvent(formId, message) {
        // 若卡片支持触发事件,则需要重写该方法并实现对事件的触发
        console.info('FormAbility onEvent');
      },
      onDestroy(formId) {
        // 删除卡片实例数据
        console.info('FormAbility onDestroy');
      },
      onAcquireFormState(want) {
        console.info('FormAbility onAcquireFormState');
        return formInfo.FormState.READY;
      },
    }
    

说明: FormAbility不能常驻后台,即在卡片生命周期回调函数中无法处理长时间的任务。

配置卡片配置文件

卡片需要在应用配置文件config.json中进行配置。

  • js模块,用于对应卡片的js相关资源,内部字段结构说明:
属性名称 含义 数据类型 是否可缺省
name 表示JS Component的名字。该标签不可缺省,默认值为default。 字符串
pages 表示JS Component的页面用于列举JS Component中每个页面的路由信息[页面路径+页面名称]。该标签不可缺省,取值为数组,数组第一个元素代表JS FA首页。 数组
window 用于定义与显示窗口相关的配置。 对象 可缺省
type 表示JS应用的类型。取值范围如下:
normal:标识该JS Component为应用实例。
form:标识该JS Component为卡片实例。
字符串 可缺省,缺省值为“normal”
mode 定义JS组件的开发模式。 对象 可缺省,缺省值为空

配置示例如下:

"js": [{
   "name": "widget",
   "pages": ["pages/index/index"],
   "window": {
       "designWidth": 720,
       "autoDesignWidth": true
   },
   "type": "form"
}]
  • abilities模块,用于对应卡片的FormAbility,内部字段结构说明:
属性名称 含义 数据类型 是否可缺省
name 表示卡片的类名。字符串最大长度为127字节。 字符串
description 表示卡片的描述。取值可以是描述性内容,也可以是对描述性内容的资源索引,以支持多语言。字符串最大长度为255字节。 字符串 可缺省,缺省为空。
isDefault 表示该卡片是否为默认卡片,每个Ability有且只有一个默认卡片。
true:默认卡片。
false:非默认卡片。
布尔值
type 表示卡片的类型。取值范围如下:
JS:JS卡片。
字符串
colorMode 表示卡片的主题样式,取值范围如下:
auto:自适应。
dark:深色主题。
light:浅色主题。
字符串 可缺省,缺省值为“auto”。
supportDimensions 表示卡片支持的外观规格,取值范围:
1 * 2:表示1行2列的二宫格。
2 * 2:表示2行2列的四宫格。
2 * 4:表示2行4列的八宫格。
4 * 4:表示4行4列的十六宫格。
字符串数组
defaultDimension 表示卡片的默认外观规格,取值必须在该卡片supportDimensions配置的列表中。 字符串
updateEnabled 表示卡片是否支持周期性刷新,取值范围:
true:表示支持周期性刷新,可以在定时刷新(updateDuration)和定点刷新(scheduledUpdateTime)两种方式任选其一,优先选择定时刷新。
false:表示不支持周期性刷新。
布尔类型
scheduledUpdateTime 表示卡片的定点刷新的时刻,采用24小时制,精确到分钟。
updateDuration参数优先级高于scheduledUpdateTime,两者同时配置时,以updateDuration配置的刷新时间为准。
字符串 可缺省,缺省值为“0:0”。
updateDuration 表示卡片定时刷新的更新周期,单位为30分钟,取值为自然数。
当取值为0时,表示该参数不生效。
当取值为正整数N时,表示刷新周期为30*N分钟。
updateDuration参数优先级高于scheduledUpdateTime,两者同时配置时,以updateDuration配置的刷新时间为准。
数值 可缺省,缺省值为“0”。
formConfigAbility 表示卡片的配置跳转链接,采用URI格式。 字符串 可缺省,缺省值为空。
formVisibleNotify 标识是否允许卡片使用卡片可见性通知。 字符串 可缺省,缺省值为空。
jsComponentName 表示JS卡片的Component名称。字符串最大长度为127字节。 字符串
metaData 表示卡片的自定义信息,包含customizeData数组标签。 对象 可缺省,缺省值为空。
customizeData 表示自定义的卡片信息。 对象数组 可缺省,缺省值为空。

配置示例如下:

   "abilities": [{
       "name": "FormAbility",
       "description": "This is a FormAbility",
       "formsEnabled": true,
       "icon": "$media:icon",
       "label": "$string:form_FormAbility_label",
       "srcPath": "FormAbility",
       "type": "service",
       "srcLanguage": "ets",
       "formsEnabled": true,
       "formConfigAbility": "ability://com.example.entry.EntryAbility",
       "forms": [{
           "colorMode": "auto",
           "defaultDimension": "2*2",
           "description": "This is a service widget.",
           "formVisibleNotify": true,
           "isDefault": true,
           "jsComponentName": "widget",
           "name": "widget",
           "scheduledUpdateTime": "10:30",
           "supportDimensions": ["2*2"],
           "type": "JS",
           "updateEnabled": true
       }]
   }]

卡片信息的持久化

因大部分卡片提供方都不是常驻服务,只有在需要使用时才会被拉起获取卡片信息,且卡片管理服务支持对卡片进行多实例管理,卡片ID对应实例ID,因此若卡片提供方支持对卡片数据进行配置,则需要对卡片的业务数据按照卡片ID进行持久化管理,以便在后续获取、更新以及拉起时能获取到正确的卡片业务数据。

const DATA_STORAGE_PATH = "/data/storage/el2/base/haps/form_store";
async function storeFormInfo(formId: string, formName: string, tempFlag: boolean) {
    // 此处仅对卡片ID:formId,卡片名:formName和是否为临时卡片:tempFlag进行了持久化
    let formInfo = {
        "formName": formName,
        "tempFlag": tempFlag,
        "updateCount": 0
    };
    try {
        const storage = await dataStorage.getStorage(DATA_STORAGE_PATH);
        // put form info
        await storage.put(formId, JSON.stringify(formInfo));
        console.info(`storeFormInfo, put form info successfully, formId: ${formId}`);
        await storage.flush();
    } catch (err) {
        console.error(`failed to storeFormInfo, err: ${JSON.stringify(err)}`);
    }
}

...
    onCreate(want) {
        console.info('FormAbility onCreate');

        let formId = want.parameters["ohos.extra.param.key.form_identity"];
        let formName = want.parameters["ohos.extra.param.key.form_name"];
        let tempFlag = want.parameters["ohos.extra.param.key.form_temporary"];
        // 将创建的卡片信息持久化,以便在下次获取/更新该卡片实例时进行使用
        // 此接口请根据实际情况实现,具体请参考:FormExtAbility Stage模型卡片实例
        storeFormInfo(formId, formName, tempFlag);

        let obj = {
            "title": "titleOnCreate",
            "detail": "detailOnCreate"
        };
        let formData = formBindingData.createFormBindingData(obj);
        return formData;
    }
...

且需要适配onDestroy卡片删除通知接口,在其中实现卡片实例数据的删除。

const DATA_STORAGE_PATH = "/data/storage/el2/base/haps/form_store";
async function deleteFormInfo(formId: string) {
    try {
        const storage = await dataStorage.getStorage(DATA_STORAGE_PATH);
        // del form info
        await storage.delete(formId);
        console.info(`deleteFormInfo, del form info successfully, formId: ${formId}`);
        await storage.flush();
    } catch (err) {
        console.error(`failed to deleteFormInfo, err: ${JSON.stringify(err)}`);
    }
}

...
    onDestroy(formId) {
        console.info('FormAbility onDestroy');
        // 删除之前持久化的卡片实例数据
        // 此接口请根据实际情况实现,具体请参考:FormExtAbility Stage模型卡片实例
        deleteFormInfo(formId);
    }
...

具体的持久化方法可以参考数据管理开发指导

需要注意的是,卡片使用方在请求卡片时传递给提供方应用的Want数据中存在临时标记字段,表示此次请求的卡片是否为临时卡片:

  • 常态卡片:卡片使用方会持久化的卡片;

  • 临时卡片:卡片使用方不会持久化的卡片;

由于临时卡片的数据具有非持久化的特殊性,某些场景例如卡片服务框架死亡重启,此时临时卡片数据在卡片管理服务中已经删除,且对应的卡片ID不会通知到提供方,所以卡片提供方需要自己负责清理长时间未删除的临时卡片数据。同时对应的卡片使用方可能会将之前请求的临时卡片转换为常态卡片。如果转换成功,卡片提供方也需要对对应的临时卡片ID进行处理,把卡片提供方记录的临时卡片数据转换为常态卡片数据,防止提供方在清理长时间未删除的临时卡片时,把已经转换为常态卡片的临时卡片信息删除,导致卡片信息丢失。

卡片数据交互

当卡片应用需要更新数据时(如触发了定时更新或定点更新),卡片应用获取最新数据,并调用updateForm()接口更新主动触发卡片的更新。

onUpdate(formId) {
    // 若卡片支持定时更新/定点更新/卡片使用方主动请求更新功能,则提供方需要重写该方法以支持数据更新
    console.info('FormAbility onUpdate');
    let obj = {
        "title": "titleOnUpdate",
        "detail": "detailOnUpdate"
    };
    let formData = formBindingData.createFormBindingData(obj);
    // 调用updateForm接口去更新对应的卡片,仅更新入参中携带的数据信息,其他信息保持不变
    formProvider.updateForm(formId, formData).catch((error) => {
        console.info('FormAbility updateForm, error:' + JSON.stringify(error));
    });
}

开发卡片页面

开发者可以使用类Web范式(HML+CSS+JSON)开发JS卡片页面。生成如下卡片页面,可以这样配置卡片页面文件:

widget-development-fa

说明: FA模型当前仅支持JS扩展的类Web开发范式来实现卡片的UI。

  • HML:使用类Web范式的组件描述卡片的页面信息。

    <div class="container">
      <stack>
        <div class="container-img">
          <image src="/common/widget.png" class="bg-img"></image>
        </div>
        <div class="container-inner">
          <text class="title">{{title}}</text>
          <text class="detail_text" onclick="routerEvent">{{detail}}</text>
        </div>
      </stack>
    </div>
    
  • CSS:HML中类Web范式组件的样式信息。

    .container {
      flex-direction: column;
      justify-content: center;
      align-items: center;
    }
    
    .bg-img {
      flex-shrink: 0;
      height: 100%;
    }
    
    .container-inner {
      flex-direction: column;
      justify-content: flex-end;
      align-items: flex-start;
      height: 100%;
      width: 100%;
      padding: 12px;
    }
    
    .title {
      font-size: 19px;
      font-weight: bold;
      color: white;
      text-overflow: ellipsis;
      max-lines: 1;
    }
    
    .detail_text {
      font-size: 16px;
      color: white;
      opacity: 0.66;
      text-overflow: ellipsis;
      max-lines: 1;
      margin-top: 6px;
    }
    
  • JSON:卡片页面中的数据和事件交互。

    {
      "data": {
        "title": "TitleDefault",
        "detail": "TextDefault"
      },
      "actions": {
        "routerEvent": {
          "action": "router",
          "abilityName": "com.example.entry.EntryAbility",
          "params": {
            "message": "add detail"
          }
        }
      }
    }
    

开发卡片事件

卡片支持为组件设置交互事件(action),包括router事件和message事件,其中router事件用于Ability跳转,message事件用于卡片开发人员自定义点击事件。关键步骤说明如下:

  1. 在hml中为组件设置onclick属性,其值对应到json文件的actions字段中。

  2. 如何设置router事件:

    • action属性值为"router";
    • abilityName为跳转目标的Ability名(支持跳转FA模型的PageAbility组件和Stage模型的UIAbility组件),如目前DevEco创建的FA模型的UIAbility默认名为com.example.entry.EntryAbility;
    • params为传递给跳转目标Ability的自定义参数,可以按需填写。其值可以在目标Ability启动时的want中的parameters里获取。如FA模型EntryAbility的onCreate生命周期里可以通过featureAbility.getWant()获取到want,然后在其parameters字段下获取到配置的参数;
  3. 如何设置message事件:

    • action属性值为"message";
    • params为message事件的用户自定义参数,可以按需填写。其值可以在卡片生命周期函数onEvent中的message里获取;

示例如下:

  • hml文件

    <div class="container">
        <stack>
            <div class="container-img">
                <image src="/common/widget.png" class="bg-img"></image>
            </div>
            <div class="container-inner">
                <text class="title" onclick="routerEvent">{{title}}</text>
                <text class="detail_text" onclick="messageEvent">{{detail}}</text>
            </div>
        </stack>
    </div>
    
  • css文件

    .container {
      flex-direction: column;
      justify-content: center;
      align-items: center;
    }
    
    .bg-img {
      flex-shrink: 0;
      height: 100%;
    }
    
    .container-inner {
      flex-direction: column;
      justify-content: flex-end;
      align-items: flex-start;
      height: 100%;
      width: 100%;
      padding: 12px;
    }
    
    .title {
      font-size: 19px;
      font-weight: bold;
      color: white;
      text-overflow: ellipsis;
      max-lines: 1;
    }
    
    .detail_text {
      font-size: 16px;
      color: white;
      opacity: 0.66;
      text-overflow: ellipsis;
      max-lines: 1;
      margin-top: 6px;
    }
    
  • json文件

    {
      "data": {
        "title": "TitleDefault",
        "detail": "TextDefault"
      },
      "actions": {
        "routerEvent": {
          "action": "router",
          "abilityName": "com.example.entry.EntryAbility",
          "params": {
            "message": "add detail"
          }
        },
        "messageEvent": {
          "action": "message",
          "params": {
            "message": "add detail"
          }
        }
      }
    }