前端成功转鸿蒙开发者真实案例,教大家如何开发鸿蒙APP--ArkTS 卡片刷新机制

大家好,我是陈杨,一名有着8 年前端开发经验、6 年技术写作积淀的鸿蒙开发者,也是鸿蒙生态里的一名极客。

曾因前端行业的危机感居安思危,果断放弃饱和的 iOS、安卓赛道,在鸿蒙 API9 发布时,凭着前端技术底子,三天吃透 ArkTS 框架 ,快速上手鸿蒙开发。三年深耕,我不仅打造了鸿蒙开源图表组件库「莓创图表」,闯进过创新赛、极客挑战赛总决赛,更带着团队实实在在做出了成果 ------ 目前已成功上架11 款鸿蒙应用 ,涵盖工具、效率、创意等多个品类,包括JLPT、REFLEX PRO、国潮纸刻、Wss 直连、ZenithDocs Pro、圣诞相册、CSS 特效等,靠这些自研产品赚到了转型后的第一桶金。

从前端转型到鸿蒙掘金,靠的不是运气,而是选对赛道的眼光和快速落地的执行力。今天这篇文章,就接着上一篇的内容,和大家聊聊 [ArkTS 卡片刷新机制],这篇很重要,认真听。

在 ArkTS 卡片开发中,"内容刷新" 是核心功能之一 ------ 无论是实时数据更新(如天气、新闻)、图片替换,还是基于状态的内容切换,都依赖高效的刷新机制。本文基于 HarmonyOS 官方文档,系统梳理卡片刷新的两种核心模式(主动刷新、被动刷新)、特殊场景实现(图片刷新、状态关联刷新)及关键约束,结合完整代码示例,帮助开发者精准落地各类刷新需求。

一、刷新机制核心概述

ArkTS 卡片的刷新能力由系统框架提供,核心依赖两套接口和配置体系:

  • 主动刷新:由卡片提供方(应用)或使用方(如桌面)主动触发,适用于 "按需更新" 场景(如用户点击刷新按钮、应用数据变化);
  • 被动刷新:由系统根据预设规则自动触发,适用于 "周期性更新" 场景(如每天固定时间刷新、每隔 30 分钟更新);
  • 数据传递 :刷新时的数据通过formBindingData封装,卡片页面通过@LocalStorageProp接收(数据自动转为 string 类型),确保数据同步的一致性。

关键接口说明:

接口名 调用方 核心作用 约束
updateForm 卡片提供方 主动刷新自身卡片内容 仅能刷新当前应用的卡片,无法操作其他应用卡片
requestForm 卡片使用方(系统应用) 主动请求刷新已添加的卡片 仅能刷新当前宿主中的卡片
setFormNextRefreshTime 卡片提供方 设置下次刷新时间 最短刷新间隔 5 分钟

二、主动刷新:按需触发的精准更新

主动刷新是开发者最常使用的模式,核心是通过updateForm接口手动触发,可搭配用户交互、应用数据变化等场景使用。

2.1 核心实现:提供方主动刷新

卡片提供方(应用)通过formProvider.updateForm接口触发刷新,需传入目标卡片的formId(唯一标识)和更新后的数据。通常与onFormEvent(用户交互触发)、onUpdateForm(生命周期回调)搭配使用。

完整代码示例:用户点击按钮触发刷新

plain 复制代码
// 1. 卡片页面(WidgetCard.ets):提供刷新按钮,通过postCardAction传递事件
let storage = new LocalStorage();
@Entry(storage)
@Component
struct RefreshButtonCard {
  // 接收FormExtensionAbility传递的formId和数据
  @LocalStorageProp('formId') formId: string = '';
  @LocalStorageProp('currentData') currentData: string = '初始数据';

  build() {
    Column({ space: 20 })
      .width('100%')
      .height('100%')
      .justifyContent(FlexAlign.Center)
      .padding(15) {

      Text(`当前内容:${this.currentData}`)
        .fontSize(16)
        .fontWeight(FontWeight.Medium)

      // 点击按钮触发刷新事件
      Button("手动刷新")
        .width("80%")
        .height(50)
        .backgroundColor("#5A5FFF")
        .fontColor("#FFFFFF")
        .onClick(() => {
          // 发送message事件给FormExtensionAbility,携带formId
          postCardAction(this, {
            action: "message",
            params: {
              formId: this.formId,
              refreshType: "manual"
            }
          });
        })
    }
  }
}

// 2. FormExtensionAbility(EntryFormAbility.ets):接收事件并执行刷新
import { 
  formBindingData, 
  FormExtensionAbility, 
  formProvider, 
  formInfo 
} from '@kit.FormKit';
import { Want, BusinessError } from '@kit.AbilityKit';
import { hilog } from '@kit.PerformanceAnalysisKit';

const TAG: string = 'EntryFormAbility';
const DOMAIN_NUMBER: number = 0xFF00;

export default class EntryFormAbility extends FormExtensionAbility {
  // 卡片创建时,将formId存入LocalStorage(供页面使用)
  onAddForm(want: Want): formBindingData.FormBindingData {
    hilog.info(DOMAIN_NUMBER, TAG, '[onAddForm] 卡片创建');
    // 获取卡片唯一ID
    const formId = want.parameters?.[formInfo.FormParam.IDENTITY_KEY] as string;
    // 初始化数据(包含formId)
    const initData = {
      formId: formId,
      currentData: '初始数据'
    };
    return formBindingData.createFormBindingData(initData);
  }

  // 接收卡片页面的message事件,执行刷新
  async onFormEvent(formId: string, message: string): Promise<void> {
    const params = JSON.parse(message);
    hilog.info(DOMAIN_NUMBER, TAG, `[onFormEvent] 触发手动刷新,formId: ${formId}`);

    // 模拟获取新数据(实际场景可替换为接口请求、数据库查询等)
    const newData = {
      currentData: `刷新于 ${new Date().toLocaleTimeString()}`
    };

    // 封装刷新数据并调用updateForm
    const formInfo = formBindingData.createFormBindingData(newData);
    try {
      await formProvider.updateForm(formId, formInfo);
      hilog.info(DOMAIN_NUMBER, TAG, `[onFormEvent] 刷新成功`);
    } catch (error) {
      const err = error as BusinessError;
      hilog.error(DOMAIN_NUMBER, TAG, `[onFormEvent] 刷新失败:code=${err.code}, msg=${err.message}`);
    }
  }
}

2.2 关键注意点

  • formId是核心标识:每个卡片实例的formId唯一,需通过want.parameters[formInfo.FormParam.IDENTITY_KEY]获取(卡片创建时);
  • 异常处理必须:updateForm可能因网络异常、权限不足失败,需通过try/catch捕获BusinessError
  • 数据格式限制:传递的数据需为 JSON 可序列化类型(字符串、数字、对象),复杂类型需手动转换。

三、被动刷新:系统驱动的自动更新

被动刷新无需手动触发,由系统根据配置自动执行,核心分为 "定时刷新""定点刷新""下次刷新" 三类,适用于周期性数据更新场景。

3.1 定时刷新:固定间隔自动更新

定时刷新通过form_config.jsonupdateDuration字段配置,单位为 "30 分钟",需配合updateEnabled: true启用。

配置示例(form_config.json)

plain 复制代码
{
  "forms": [
    {
      "name": "WeatherWidget",
      "src": "./ets/widget/pages/WeatherCard.ets",
      "uiSyntax": "arkts",
      "updateEnabled": true, // 启用周期性刷新
      "updateDuration": 2, // 刷新周期:2 * 30分钟 = 1小时
      "defaultDimension": "2*2",
      "supportDimensions": ["2*2"],
      "colorMode": "auto",
      "isDefault": true
    }
  ]
}

代码实现(EntryFormAbility.ets)

系统触发定时刷新时,会回调onUpdateForm方法,需在此方法中实现数据更新逻辑:

plain 复制代码
export default class EntryFormAbility extends FormExtensionAbility {
  // 定时刷新触发时执行
  async onUpdateForm(formId: string): void {
    hilog.info(DOMAIN_NUMBER, TAG, `[onUpdateForm] 定时刷新触发,formId: ${formId}`);

    // 模拟请求天气数据(实际场景替换为真实接口)
    const newWeatherData = {
      city: "北京",
      temperature: "25℃",
      updateTime: new Date().toLocaleTimeString()
    };

    const formInfo = formBindingData.createFormBindingData(newWeatherData);
    try {
      await formProvider.updateForm(formId, formInfo);
    } catch (error) {
      const err = error as BusinessError;
      hilog.error(DOMAIN_NUMBER, TAG, `定时刷新失败:${err.message}`);
    }
  }
}

定时刷新核心约束

  • 刷新周期规则:updateDuration为自然数,0 表示不生效;API 11 及以上版本,若应用市场配置了刷新周期,取 "配置值" 和 "应用配置值" 的较大者(如应用配置 1 小时,市场配置 2 小时,则按 2 小时刷新);
  • 配额限制:每张卡片每天最多定时刷新 50 次(包含updateDurationsetFormNextRefreshTime两种方式),0 点重置;
  • 可见性影响:卡片不可见时,系统仅记录刷新动作,待卡片可见后统一刷新布局。

3.2 定点刷新:指定时间自动更新

定点刷新支持 "单时间点" 和 "多时间点" 配置,通过form_config.jsonscheduledUpdateTime(单时间点)或multiScheduledUpdateTime(多时间点)字段设置,需关闭定时刷新(updateDuration: 0`)。

配置示例(多时间点刷新)

plain 复制代码
{
  "forms": [
    {
      "name": "NewsWidget",
      "src": "./ets/widget/pages/NewsCard.ets",
      "uiSyntax": "arkts",
      "updateEnabled": true,
      "updateDuration": 0, // 关闭定时刷新,优先定点刷新
      "scheduledUpdateTime": "10:30", // 单时间点(兼容旧版本)
      "multiScheduledUpdateTime": "08:00,12:00,18:00", // 多时间点(最多24个)
      "defaultDimension": "2*4",
      "supportDimensions": ["2*4"],
      "isDefault": true
    }
  ]
}

关键说明

  • 优先级:多时间点配置(multiScheduledUpdateTime)优先级高于单时间点(scheduledUpdateTime),两者同时配置时仅多时间点生效;
  • 时间格式:采用 24 小时制,精确到分钟(如08:0016:30),多时间点用英文逗号分隔;
  • 触发逻辑:系统在指定时间点回调onUpdateForm方法,数据更新逻辑与定时刷新一致。

3.3 下次刷新:自定义延迟更新

通过formProvider.setFormNextRefreshTime接口设置下次刷新时间,适用于 "延迟更新" 场景(如用户操作后 5 分钟刷新),最短间隔为 5 分钟。

代码示例

plain 复制代码
export default class EntryFormAbility extends FormExtensionAbility {
  onFormEvent(formId: string, message: string): void {
    const params = JSON.parse(message);
    if (params.action === "setNextRefresh") {
      // 设置5分钟后刷新(参数单位:分钟)
      const delayMinutes = 5;
      formProvider.setFormNextRefreshTime(formId, delayMinutes, (err: BusinessError) => {
        if (err) {
          hilog.error(DOMAIN_NUMBER, TAG, `设置下次刷新失败:${err.message}`);
          return;
        }
        hilog.info(DOMAIN_NUMBER, TAG, `已设置5分钟后刷新`);
      });
    }
  }

  // 下次刷新时间到后,触发onUpdateForm
  async onUpdateForm(formId: string): void {
    hilog.info(DOMAIN_NUMBER, TAG, `[onUpdateForm] 下次刷新触发`);
    // 执行数据更新逻辑...
  }
}

四、特殊场景刷新:图片与状态关联

除基础文本数据刷新外,卡片常见的复杂场景包括 "图片刷新"(本地 / 网络图片)和 "状态关联刷新"(根据卡片配置刷新不同内容),需针对性处理。

4.1 图片刷新:本地与网络图片更新

卡片展示图片需通过formImages字段传递文件描述符(fd),页面通过memory://fileName协议加载,支持本地图片和网络图片两种场景。

4.1.1 本地图片刷新(卡片创建时加载)

plain 复制代码
// EntryFormAbility.ets:onAddForm中加载本地图片
import { fileIo } from '@kit.CoreFileKit';
import { Want, BusinessError } from '@kit.AbilityKit';

export default class EntryFormAbility extends FormExtensionAbility {
  onAddForm(want: Want): formBindingData.FormBindingData {
    // 获取应用临时目录(存放本地图片)
    const tempDir = this.context.getApplicationContext().tempDir;
    const imgMap: Record<string, number> = {};

    try {
      // 打开本地图片(假设tempDir下有head.png)
      const file = fileIo.openSync(`${tempDir}/head.png`);
      imgMap['avatar'] = file.fd; // fd为文件描述符,作为图片标识
    } catch (e) {
      const err = e as BusinessError;
      hilog.error(DOMAIN_NUMBER, TAG, `打开本地图片失败:${err.message}`);
    }

    // 封装图片数据(formImages为固定字段,不可改名)
    class FormData {
      text: string = "我的头像";
      imgName: string = "avatar"; // 与formImages的key一致
      formImages: Record<string, number> = imgMap; // 存储fd
    }

    return formBindingData.createFormBindingData(new FormData());
  }
}

// 卡片页面(WidgetCard.ets):加载本地图片
let storage = new LocalStorage();
@Entry(storage)
@Component
struct LocalImageCard {
  @LocalStorageProp('text') text: string = "加载中...";
  @LocalStorageProp('imgName') imgName: string = "";

  build() {
    Column()
      .width('100%')
      .height('100%')
      .justifyContent(FlexAlign.Center) {

      // 通过memory://协议加载图片(imgName对应formImages的key)
      Image(`memory://${this.imgName}`)
        .width(100)
        .height(100)
        .borderRadius(50)
        .objectFit(ImageFit.Cover)

      Text(this.text)
        .margin({ top: 10 })
        .fontSize(14)
    }
  }
}

4.1.2 网络图片刷新(用户点击触发)

网络图片需先下载到本地临时目录,再通过 fd 传递,需申请ohos.permission.INTERNET权限。

plain 复制代码
// 1. 配置权限(module.json5)
{
  "module": {
    "requestPermissions": [
      {
        "name": "ohos.permission.INTERNET",
        "reason": "用于下载网络图片",
        "usedScene": { "abilities": ["EntryFormAbility"], "when": "always" }
      }
    ]
  }
}

// 2. EntryFormAbility.ets:onFormEvent中下载并刷新图片
import { http } from '@kit.NetworkKit';

export default class EntryFormAbility extends FormExtensionAbility {
  async onFormEvent(formId: string, message: string): Promise<void> {
    // 先更新状态为"刷新中"
    let loadingData = { text: "刷新中..." };
    await formProvider.updateForm(formId, formBindingData.createFormBindingData(loadingData));

    // 网络图片地址(替换为真实链接)
    const imgUrl = "https://example.com/new-avatar.jpg";
    const tempDir = this.context.getApplicationContext().tempDir;
    const fileName = `img_${Date.now()}`; // 文件名唯一(确保图片刷新)
    const tempFile = `${tempDir}/${fileName}`;
    const imgMap: Record<string, number> = {};

    try {
      // 1. 下载网络图片
      const httpRequest = http.createHttp();
      const response = await httpRequest.request(imgUrl);
      if (response.responseCode !== http.ResponseCode.OK) {
        throw new Error(`下载失败:${response.responseCode}`);
      }

      // 2. 写入临时文件
      const imgFile = fileIo.openSync(tempFile, fileIo.OpenMode.READ_WRITE | fileIo.OpenMode.CREATE);
      await fileIo.write(imgFile.fd, response.result as ArrayBuffer);
      imgMap[fileName] = imgFile.fd;

      // 3. 刷新卡片图片
      const formData = {
        text: "刷新成功",
        imgName: fileName,
        formImages: imgMap
      };
      await formProvider.updateForm(formId, formBindingData.createFormBindingData(formData));

      // 4. 关闭文件(需在刷新后执行)
      fileIo.closeSync(imgFile);
      httpRequest.destroy();
    } catch (error) {
      const err = error as BusinessError;
      hilog.error(DOMAIN_NUMBER, TAG, `网络图片刷新失败:${err.message}`);
      // 刷新失败状态
      const failData = { text: "刷新失败" };
      await formProvider.updateForm(formId, formBindingData.createFormBindingData(failData));
    }
  }
}

图片刷新关键约束

  • 图片大小限制:展示的图片需控制在 2MB 以内,避免占用过多内存;
  • 文件名唯一性:每次刷新需使用不同的imgName(如加时间戳),否则卡片不会感知图片变化;
  • 下载超时限制:FormExtensionAbility后台仅存活 5 秒,需确保网络图片快速下载完成。

4.2 状态关联刷新:根据卡片配置更新内容

同一卡片可能存在多种状态(如两张天气卡片分别显示北京、上海天气),需通过持久化存储记录卡片状态,刷新时根据状态返回对应内容。

完整实现示例

plain 复制代码
// 1. form_config.json:配置定时刷新(每30分钟)
{
  "forms": [
    {
      "name": "CityWeatherWidget",
      "src": "./ets/widget/pages/WeatherCard.ets",
      "uiSyntax": "arkts",
      "updateEnabled": true,
      "updateDuration": 1, // 30分钟刷新一次
      "defaultDimension": "2*2",
      "supportDimensions": ["2*2"],
      "isDefault": true
    }
  ]
}

// 2. 卡片页面(WeatherCard.ets):选择城市状态
let storage = new LocalStorage();
@Entry(storage)
@Component
struct CityWeatherCard {
  @LocalStorageProp('beijingWeather') beijingWeather: string = "待刷新";
  @LocalStorageProp('shanghaiWeather') shanghaiWeather: string = "待刷新";
  @State selectBeijing: boolean = false;
  @State selectShanghai: boolean = false;

  build() {
    Column({ space: 15 })
      .width('100%')
      .height('100%')
      .padding(15) {

      // 城市选择复选框
      Row({ space: 8 }) {
        Checkbox({ name: 'bj', group: 'cityGroup' })
          .onChange((value) => {
            this.selectBeijing = value;
            // 通知FormExtensionAbility更新状态
            this.notifyStateChange();
          });
        Text("北京")
          .fontSize(14);
      }

      Row({ space: 8 }) {
        Checkbox({ name: 'sh', group: 'cityGroup' })
          .onChange((value) => {
            this.selectShanghai = value;
            this.notifyStateChange();
          });
        Text("上海")
          .fontSize(14);
      }

      // 天气展示
      Text(`北京天气:${this.beijingWeather}`)
        .fontSize(14)
        .margin({ top: 10 });
      Text(`上海天气:${this.shanghaiWeather}`)
        .fontSize(14);
    }
  }

  // 发送状态变更事件
  private notifyStateChange() {
    postCardAction(this, {
      action: "message",
      params: {
        selectBeijing: JSON.stringify(this.selectBeijing),
        selectShanghai: JSON.stringify(this.selectShanghai)
      }
    });
  }
}

// 3. EntryFormAbility.ets:持久化状态并刷新对应内容
import { preferences } from '@kit.ArkData';

export default class EntryFormAbility extends FormExtensionAbility {
  // 卡片创建时初始化状态(存入preferences数据库)
  async onAddForm(want: Want): formBindingData.FormBindingData {
    const formId = want.parameters?.[formInfo.FormParam.IDENTITY_KEY] as string;
    const isTempCard = want.parameters?.[formInfo.FormParam.TEMPORARY_KEY] as boolean;

    // 仅常态卡片持久化状态
    if (!isTempCard) {
      const store = await preferences.getPreferences(this.context, 'cityWeatherStore');
      await store.put(`bj_${formId}`, 'false'); // 北京状态:未选中
      await store.put(`sh_${formId}`, 'false'); // 上海状态:未选中
      await store.flush();
    }

    return formBindingData.createFormBindingData({});
  }

  // 接收状态变更事件,更新数据库
  async onFormEvent(formId: string, message: string): Promise<void> {
    const params = JSON.parse(message);
    const store = await preferences.getPreferences(this.context, 'cityWeatherStore');

    if (params.selectBeijing !== undefined) {
      await store.put(`bj_${formId}`, params.selectBeijing);
    }
    if (params.selectShanghai !== undefined) {
      await store.put(`sh_${formId}`, params.selectShanghai);
    }
    await store.flush();
  }

  // 定时刷新时,根据状态返回对应天气数据
  async onUpdateForm(formId: string): void {
    const store = await preferences.getPreferences(this.context, 'cityWeatherStore');
    const selectBeijing = await store.get(`bj_${formId}`, 'false');
    const selectShanghai = await store.get(`sh_${formId}`, 'false');

    const updateData: Record<string, string> = {};
    // 模拟获取天气数据
    if (selectBeijing === 'true') {
      updateData.beijingWeather = `25℃ 晴(${new Date().toLocaleTimeString()})`;
    }
    if (selectShanghai === 'true') {
      updateData.shanghaiWeather = `28℃ 多云(${new Date().toLocaleTimeString()})`;
    }

    await formProvider.updateForm(formId, formBindingData.createFormBindingData(updateData));
  }

  // 卡片删除时,清理数据库数据
  async onRemoveForm(formId: string): void {
    const store = await preferences.getPreferences(this.context, 'cityWeatherStore');
    await store.delete(`bj_${formId}`);
    await store.delete(`sh_${formId}`);
  }
}

五、刷新机制核心约束与最佳实践

5.1 关键约束(避坑重点)

  1. 刷新次数限制:定时刷新每天最多 50 次,超出后当天无法触发;
  2. 进程存活限制FormExtensionAbility后台仅存活 5 秒(触发回调后),耗时操作(如下载大文件)需拉起主应用处理;
  3. 权限限制 :网络图片刷新需申请INTERNET权限,本地文件操作需确保路径合法;
  4. 资源限制:图片大小≤2MB,自定义字体总大小≤20MB,避免内存溢出。

5.2 最佳实践

  1. 选择合适的刷新方式
  • 实时交互场景(如点击刷新)→ 主动刷新;
  • 周期性数据(如天气、新闻)→ 被动刷新(定时 / 定点);
  1. 优化刷新性能
  • 减少刷新频率,非必要不使用高频定时刷新;
  • 刷新数据仅传递变更部分,避免全量更新;
  1. 异常处理兜底
  • 网络请求失败时,保留上次有效数据;
  • 图片加载失败时,显示默认占位图;
  1. 清理冗余数据:卡片删除时,同步清理数据库、临时文件等,避免存储冗余。

总结

ArkTS 卡片的刷新机制围绕 "主动按需" 和 "被动自动" 两大核心,覆盖了文本、图片、状态关联等各类场景。开发时需重点关注formId唯一性、数据传递格式、系统约束(次数、内存、进程存活),并根据业务场景选择合适的刷新方式。通过本文的代码示例和约束说明,可快速落地稳定、高效的卡片刷新功能,提升用户体验。

相关推荐
恋猫de小郭18 分钟前
AI 可以让 WIFI 实现监控室内人体位置和姿态,无需摄像头?
前端·人工智能·ai编程
哀木23 分钟前
给自己整一个 claude code,解锁编程新姿势
前端
程序员鱼皮27 分钟前
GitHub 关注突破 2w,我总结了 10 个涨星涨粉技巧!
前端·后端·github
UrbanJazzerati30 分钟前
Vue3 父子组件通信完全指南
前端·面试
SummerKaze35 分钟前
为鸿蒙开发者写一个 nvm:hmvm 的设计与实现
harmonyos
是一碗螺丝粉40 分钟前
5分钟上手LangChain.js:用DeepSeek给你的App加上AI能力
前端·人工智能·langchain
wuhen_n41 分钟前
双端 Diff 算法详解
前端·javascript·vue.js
UrbanJazzerati41 分钟前
Vue 3 纯小白快速入门指南
前端·面试
雮尘41 分钟前
手把手带你玩转Android gRPC:一篇搞定原理、配置与客户端开发
android·前端·grpc
光影少年42 分钟前
说说闭包的理解和应用场景?
前端·javascript·掘金·金石计划