在这里插入图片描述

HarmonyOS技术精讲-Form Kit(卡片开发服务)第4篇:卡片数据更新机制——定时刷新与事件驱动

问题来了:卡片数据怎么动起来?

很多人第一次接触HarmonyOS卡片开发时,发现卡片做好了,但数据是死的。温度永远是25度,天气永远是晴天——即使外面的太阳已经晒得人发晕,卡片上的信息纹丝不动。

这个问题并不是个别案例。Form Kit(卡片开发服务) 提供了两种数据更新方式:定时刷新和事件驱动。前者通过配置 updateDuration 让系统自动拉新数据,后者通过 formProvider 在应用内部主动触发更新。但真正难的是:什么时候用定时,什么时候用手动,以及两者如何配合。

这篇文章用一个模拟天气卡片来拆解这个问题。

两种更新方式,适合的场景完全不同

特性 定时刷新 (updateDuration) 事件驱动 (formProvider.updateForm)
触发方式 系统周期拉取,与应用生命周期无关 应用主动触发,依赖进程存活
更新频率 固定时间间隔,最小30分钟 任意时间点,可秒级更新
适用场景 天气、新闻、日历等周期性数据 待办完成、开关切换、支付结果
缺点 频率受限,无法即时响应 应用被杀后失效
推荐用法 作为保底策略 作为即时响应策略

这句是重点:实际项目里很少有只用一种方式的场景。天气卡片既要每小时自动刷新温度,也要支持用户点击刷新按钮即时更新。两者不是二选一,而是互补。

环境说明

DevEco Studio 版本:DevEco Studio 6.1.0 及以上
HarmonyOS SDK 版本:HarmonyOS 6.1.0(23) 及以上
目标设备:手机 / 平板

核心实现:天气卡片实时更新

第一步:创建卡片FormAbility

卡片的所有逻辑入口在 FormAbility 中。这里要重点理解:卡片不是常驻进程,它由 FormAbility 托管生命周期。定时更新和事件驱动最终都会落到 onUpdateForm 方法。

// EntryFormAbility.ts
import { FormBindingData, formProvider, FormAbility } from '@kit.FormKit';
import { JSON } from '@kit.ArkTS';

export default class EntryFormAbility extends FormAbility {
  // 卡片被添加时触发
  onAddForm(want): FormBindingData {
    let formData = this.getMockWeatherData();
    return formProvider.createFormBindingData({
      temperature: formData.temperature,
      weather: formData.weather,
      city: formData.city,
      updateTime: new Date().toLocaleString()
    });
  }

  // 定时更新与事件驱动都会触发此方法
  onUpdateForm(formId: string): void {
    let formData = this.getMockWeatherData();
    let newData = {
      temperature: formData.temperature,
      weather: formData.weather,
      city: formData.city,
      updateTime: new Date().toLocaleString()
    };
    formProvider.updateForm(formId, 
      formProvider.createFormBindingData(newData))
      .then(() => {
        console.info('卡片更新成功');
      })
      .catch((err: Error) => {
        console.error('卡片更新失败: ' + JSON.stringify(err));
      });
  }

  // 模拟天气数据
  private getMockWeatherData(): object {
    let temperature = Math.floor(Math.random() * 15 + 20); // 20-35度
    let weathers = ['晴', '多云', '阴', '小雨'];
    let weather = weathers[Math.floor(Math.random() * weathers.length)];
    let cities = ['北京', '上海', '广州', '深圳'];
    let city = cities[Math.floor(Math.random() * cities.length)];
    return { temperature, weather, city };
  }
}

代码解读

  • onAddForm 只在卡片首次添加到桌面时执行一次,用于初始化数据。
  • onUpdateForm 是核心入口,定时和手动都会走到这里。
  • formProvider.updateForm 是真正的更新动作,第2个参数必须传 createFormBindingData 返回的对象。
  • 模拟数据是为了演示,实际项目里一般从网络或本地数据库获取。

第二步:配置定时更新

定时更新不需要写任何定时器代码,只需在 module.json5 中配置 updateDuration

// entry/src/main/module.json5
{
  "module": {
    "abilities": [
      {
        "name": "EntryFormAbility",
        "type": "form",
        "formConfig": {
          "updateEnabled": true,
          "updateDuration": 60, // 单位:分钟,最小30分钟
          "formVisibleNotify": true
        }
      }
    ]
  }
}

注意事项

  • updateEnabled 必须为 true,否则定时更新不生效。
  • updateDuration 单位是分钟,最小值为30。设置60表示每小时更新一次,但实测可能会有几分钟的延迟,这是系统功耗管理机制导致的。
  • formVisibleNotify 建议设置为 true,这样系统只会在卡片可见时触发更新,避免后台浪费资源。

第三步:添加点击更新按钮

定时更新解决了周期性问题,但用户点击卡片上的刷新按钮时,需要立即更新。这个需要在卡片UI中处理。

// widget/pages/WeatherCard.ets
import { formProvider, postCardAction } from '@kit.FormKit';

@Component
export default struct WeatherCard {
  @State temperature: string = '--';
  @State weather: string = '--';
  @State city: string = '--';
  @State updateTime: string = '--';

  build() {
    Column() {
      // 城市名称
      Text(this.city)
        .fontSize(14)
        .fontWeight(FontWeight.Bold)
        .margin({ top: 12 })
      
      // 温度,支持点击触发事件
      Text(this.temperature + '°C')
        .fontSize(48)
        .fontWeight(FontWeight.Bold)
        .margin({ top: 8 })
        .onClick(() => {
          // 发送事件通知FormAbility更新数据
          postCardAction(this, {
            action: 'message',
            params: {
              type: 'refresh'
            }
          });
        })
      
      // 天气描述
      Text(this.weather)
        .fontSize(16)
        .margin({ top: 4 })
      
      // 更新时间
      Text('更新于: ' + this.updateTime)
        .fontSize(10)
        .fontColor('#999999')
        .margin({ top: 8 })
      
      // 刷新按钮
      Button('刷新')
        .width(60)
        .height(28)
        .fontSize(12)
        .margin({ top: 8 })
        .onClick(() => {
          postCardAction(this, {
            action: 'message',
            params: {
              type: 'refresh'
            }
          });
        })
    }
    .width('100%')
    .height('100%')
    .alignItems(HorizontalAlign.Center)
    .backgroundColor('#F0F8FF')
  }
}

然后需要在 FormAbility 中处理这个事件:

// EntryFormAbility.ts - 添加消息处理方法
export default class EntryFormAbility extends FormAbility {
  // 处理卡片发送的事件
  onFormEvent(formId: string, message: string): void {
    let params: Record<string, string> = JSON.parse(message);
    if (params.type === 'refresh') {
      // 手动触发更新
      this.onUpdateForm(formId);
    }
  }
}

这里有个关键点:卡片内的 onClick 不能直接调用 formProvider.updateForm,因为卡片运行在独立的渲染进程中。必须通过 postCardAction 将事件发送给 FormAbility,由它在宿主进程中执行更新逻辑。

第四步:实现数据获取逻辑

前面用的 getMockWeatherData 只是模拟,实际项目要从网络或本地获取。以下是一个带网络请求的版本:

// services/WeatherService.ts
import { http } from '@kit.NetworkKit';

export namespace WeatherService {
  // 真实项目里使用真实API
  export async function fetchWeather(city: string): Promise<object> {
    try {
      let response = await http.createHttp().request(
        `https://api.weather.com/v1/${city}`,
        {
          method: http.RequestMethod.GET,
          connectTimeout: 5000,
          readTimeout: 5000
        }
      );
      if (response.responseCode === 200) {
        let body = JSON.parse(response.result);
        return {
          temperature: body.temperature,
          weather: body.weather,
          city: city
        };
      }
    } catch (error) {
      // 网络失败时返回兜底数据
      console.error('网络请求失败: ' + JSON.stringify(error));
    }
    return { temperature: '--', weather: '--', city: city };
  }
}

FormAbility 中使用:

// EntryFormAbility.ts
import { WeatherService } from '../services/WeatherService';

export default class EntryFormAbility extends FormAbility {
  async onUpdateForm(formId: string): Promise<void> {
    let formData = await WeatherService.fetchWeather('beijing');
    formProvider.updateForm(formId, 
      formProvider.createFormBindingData(formData));
  }
}

注意onUpdateForm 现在是 async 函数,返回 Promise<void>。系统会等待 Promise 完成后才认为更新结束,所以网络请求的超时时间要合理设置,避免长时间阻塞。

常见问题 1:定时更新不生效

现象:配置了 updateDuration,但卡片数据从未自动更新。

原因:最常见的原因是应用进程被系统杀死。卡片 FormAbility 的定时更新依赖 Ability 进程存活,如果用户的设备内存不足,系统会回收后台进程。另外,updateDuration 是以分钟为单位,但系统实际触发时间可能与配置有偏差(±5分钟),这是功耗管理策略。

解决方案

  1. 确保 updateEnabledformVisibleNotify 都配置正确。
  2. 如果定时更新无法满足业务需求,考虑使用后台任务 WorkScheduler 保活。
  3. 实际项目中建议将定时更新作为保底策略,事件驱动作为主要更新方式。

常见问题 2:即使手动点击更新,UI也没有变化

现象formProvider.updateForm 调用成功(日志输出"更新成功"),但卡片上的温度没有变化。

原因updateForm 成功后,系统不会立即刷新卡片UI。ArkUI 卡片的数据绑定有一个延迟生效机制,大概在100-300ms后才能看到效果。另外,如果连续多次调用 updateForm,系统只会应用最后一次的结果,中间的更新会被丢弃。

解决方案

  1. 不要短时间内频繁调用 updateForm,建议加防抖处理。
  2. 如果需要连续更新,每次调用之间至少间隔500ms。
  3. 卡片UI中的 @State 变量名必须与 createFormBindingData 中的键名完全一致,否则绑定失效。

最佳实践

  1. 合理设置定时更新频率
    官方文档说最小30分钟,但实际测试发现,如果用户的设备处于省电模式,更新周期可能延长到60分钟以上。对于天气类卡片,60分钟的更新频率已经足够;对于股票类卡片,30分钟是合理的折中。

  2. 事件驱动与定时更新配合使用
    定时更新保证卡片在无人操作时也有新数据,事件驱动保证用户点击后立即反馈。两者结合才是完整的解决方案。不要在 onFormEvent 中重复实现数据获取逻辑,直接复用 onUpdateForm

  3. 数据获取失败要有兜底逻辑
    网络不稳定时,fetchWeather 可能失败。如果在 onUpdateForm 中抛出异常且未捕获,系统会认为更新失败,卡片数据保持不变。建议在所有异步方法中加入 try-catch,失败时返回默认数据。

  4. 不要在主线程做耗时操作
    onUpdateForm 是在主线程中被调用,如果在这里执行网络请求,会阻塞其他事件处理。正确的做法是使用异步方法(async/await)或将耗时操作放到子线程。

Demo入口

// entry/src/main/ets/entryability/EntryAbility.ts
import { Ability } from '@kit.AbilityKit';

export default class EntryAbility extends Ability {
  onCreate(want, launchParam): void {
    // 应用启动逻辑
  }
}

卡片UI入口:

// widget/pages/WeatherCard.ets
@Entry
@Component
struct Index {
  build() {
    WeatherCard()
  }
}

示例代码地址:项目地址

FAQ

Q:updateDuration 设置15分钟可以吗?
A:不可以。官方限制最小值为30分钟,设置小于30的值会被系统强制调整为30。如果需要更快的更新频率,必须使用事件驱动方式。

Q:为什么真机上定时更新正常,模拟器上却不生效?
A:模拟器中的系统服务并不完整,尤其是功耗管理相关的服务。定时更新在模拟器上经常表现出随机行为,建议在所有开发阶段都以真机为准。

Q:应用进程被杀后,事件驱动更新就失效了,怎么办?
A:这是 Form Kit(卡片开发服务) 的设计限制。如果你需要应用被杀后卡片仍能更新,可以配置 formVisibleNotifytrue,并在 onUpdateForm 中实现数据获取逻辑。系统在卡片可见时仍会尝试调用 onUpdateForm,前提是应用进程被重新拉起。但拉起会有延迟,不可控。

Q:createFormBindingData 中可以传递复杂对象吗?
A:可以,但要确保对象可以被序列化为JSON。不支持传递函数、Symbol等非序列化数据。对象中的Key对应的Value类型必须是字符串、数字、布尔值、数组或对象。

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐