HarmonyOS服务卡片开发:动态卡片与数据绑定实战指南

✨ 一、服务卡片概述与优势

服务卡片是HarmonyOS提供的一种轻量级UI组件,具有以下核心特性:

  • 信息前置:将应用关键信息直接展示在桌面、锁屏等位置,用户无需打开应用即可获取重要信息。
  • 交互便捷:支持按钮点击等基础操作,实现功能快捷访问。
  • 多端适配:支持手机、平板、PC、智慧屏、智能手表、车机等多种设备(轻量级智能穿戴设备除外)。
  • 开发高效:借助IDE和SDK的自动化模板配置,可以快速完成卡片开发。

🛠️ 二、基础环境配置

1. 权限声明

module.json5中声明必要的权限和配置:

{
  "module": {
    "requestPermissions": [
      {
        "name": "ohos.permission.REQUIRE_FORM"
      }
    ],
    "abilities": [
      {
        "name": "EntryFormAbility",
        "type": "extension",
        "extensionAbilityType": "form",
        "metadata": [
          {
            "name": "ohos.extension.form",
            "resource": "$profile:form_config"
          }
        ]
      }
    ]
  }
}

2. 卡片配置文件

src/main/resources/base/profile/form_config.json中配置卡片属性:

{
  "forms": [
    {
      "name": "widget",
      "displayName": "$string:widget_display_name",
      "description": "$string:widget_desc",
      "src": "./ets/widget/pages/WidgetCard.ets",
      "uiSyntax": "arkts",
      "window": {
        "designWidth": 720,
        "autoDesignWidth": true
      },
      "colorMode": "auto",
      "isDynamic": true,
      "isDefault": true,
      "updateEnabled": true,
      "scheduledUpdateTime": "10:30",
      "updateDuration": 1,
      "defaultDimension": "2 * 4",
      "supportDimensions": ["2 * 2", "2 * 4", "4 * 4"]
    }
  ]
}

🔄 三、动态卡片实现

1. 创建FormExtensionAbility

import { FormExtensionAbility, formBindingData, formProvider } from '@kit.FormKit';
import { Want } from '@kit.AbilityKit';
import { BusinessError } from '@ohos.base';
import hilog from '@ohos.hilog';

const DOMAIN = 0x00001;
const TAG = 'EntryFormAbility';

export default class EntryFormAbility extends FormExtensionAbility {
  // 卡片创建时调用
  onAddForm(want: Want): formBindingData.FormBindingData {
    hilog.info(DOMAIN, TAG, 'onAddForm called');
    
    const formId = want.parameters?.['ohos.extra.param.key.form_identity'] as string;
    const formName = want.parameters?.['ohos.extra.param.key.form_name'] as string;
    
    // 初始化卡片数据
    const initialData = {
      title: '动态卡片',
      content: '初始内容',
      updateTime: new Date().toLocaleTimeString(),
      formId: formId
    };
    
    return formBindingData.createFormBindingData(initialData);
  }

  // 卡片事件处理
  onFormEvent(formId: string, message: string): void {
    hilog.info(DOMAIN, TAG, `onFormEvent: formId=${formId}, message=${message}`);
    
    try {
      const params = JSON.parse(message);
      this.handleFormEvent(formId, params);
    } catch (error) {
      hilog.error(DOMAIN, TAG, `解析消息失败: ${(error as BusinessError).message}`);
    }
  }

  // 处理具体事件
  private async handleFormEvent(formId: string, params: any): Promise<void> {
    if (params.action === 'refresh') {
      await this.refreshCardData(formId);
    } else if (params.action === 'updateContent') {
      await this.updateCardContent(formId, params.content);
    }
  }

  // 刷新卡片数据
  private async refreshCardData(formId: string): Promise<void> {
    const newData = {
      title: '动态卡片',
      content: `刷新时间: ${new Date().toLocaleTimeString()}`,
      updateTime: new Date().toLocaleTimeString()
    };
    
    await this.updateFormData(formId, newData);
  }

  // 更新卡片内容
  private async updateCardContent(formId: string, content: string): Promise<void> {
    const newData = {
      content: content,
      updateTime: new Date().toLocaleTimeString()
    };
    
    await this.updateFormData(formId, newData);
  }

  // 通用数据更新方法
  private async updateFormData(formId: string, data: any): Promise<void> {
    try {
      const formBinding = formBindingData.createFormBindingData(data);
      await formProvider.updateForm(formId, formBinding);
      hilog.info(DOMAIN, TAG, '卡片数据更新成功');
    } catch (error) {
      hilog.error(DOMAIN, TAG, `更新卡片失败: ${(error as BusinessError).message}`);
    }
  }

  // 其他生命周期方法
  onRemoveForm(formId: string): void {
    hilog.info(DOMAIN, TAG, `卡片移除: ${formId}`);
  }
}

2. 实现动态卡片UI组件

// WidgetCard.ets
import { postCardAction } from '@kit.FormKit';

@Entry
@Component
struct WidgetCard {
  @LocalStorageProp('title') title: string = '默认标题';
  @LocalStorageProp('content') content: string = '默认内容';
  @LocalStorageProp('updateTime') updateTime: string = '';
  @LocalStorageProp('formId') formId: string = '';

  build() {
    Column() {
      // 标题区域
      Text(this.title)
        .fontSize(18)
        .fontWeight(FontWeight.Bold)
        .margin({ top: 10, bottom: 5 })
        .textAlign(TextAlign.Center)

      // 内容区域
      Text(this.content)
        .fontSize(16)
        .margin({ bottom: 10 })
        .textAlign(TextAlign.Center)

      // 更新时间
      Text(`更新: ${this.updateTime}`)
        .fontSize(12)
        .fontColor(Color.Gray)
        .margin({ bottom: 10 })

      // 操作按钮
      Row() {
        Button('刷新')
          .onClick(() => this.sendMessage('refresh'))
          .margin(5)

        Button('更新')
          .onClick(() => this.sendMessage('updateContent', { content: '新内容' }))
          .margin(5)
      }
      .margin({ top: 10 })
    }
    .width('100%')
    .height('100%')
    .padding(10)
    .backgroundColor(Color.White)
  }

  // 发送消息到FormExtensionAbility
  private sendMessage(action: string, params?: object): void {
    const message = {
      action: action,
      formId: this.formId,
      ...params
    };

    postCardAction(this, {
      action: 'message',
      params: JSON.stringify(message)
    });
  }
}

📊 四、数据绑定与状态管理

1. LocalStorage数据绑定

// 创建LocalStorage实例
const localStorage = new LocalStorage();

@Entry(localStorage)
@Component
struct MainWidgetCard {
  @LocalStorageProp('title') title: string = '';
  @LocalStorageProp('data') @Watch('onDataChange') data: object = {};

  // 数据变化监听
  onDataChange(): void {
    console.info('数据已更新:', this.data);
  }

  build() {
    Column() {
      // 动态数据展示
      if (this.data.items && this.data.items.length > 0) {
        ForEach(this.data.items, (item: any) => {
          Text(item.text)
            .fontSize(14)
            .margin(2)
        })
      }

      // 数据操作按钮
      Button('添加项目')
        .onClick(() => this.addItem())
    }
  }

  private addItem(): void {
    // 通过postCardAction发送数据更新请求
    postCardAction(this, {
      action: 'message',
      params: JSON.stringify({
        action: 'addItem',
        text: `项目${Date.now()}`
      })
    });
  }
}

2. 实时数据更新机制

// 在FormExtensionAbility中实现定时更新
export default class DataFormAbility extends FormExtensionAbility {
  private timers: Map<string, number> = new Map();

  onAddForm(want: Want): formBindingData.FormBindingData {
    const formId = want.parameters?.['ohos.extra.param.key.form_identity'] as string;
    
    // 启动定时更新
    this.startPeriodicUpdate(formId);
    
    return formBindingData.createFormBindingData({
      items: this.getInitialData()
    });
  }

  private startPeriodicUpdate(formId: string): void {
    // 每隔30秒更新一次数据
    const timer = setInterval(async () => {
      await this.updateFormData(formId);
    }, 30000);

    this.timers.set(formId, timer);
  }

  private async updateFormData(formId: string): Promise<void> {
    const newData = {
      items: this.fetchLatestData(),
      updateTime: new Date().toLocaleTimeString()
    };

    try {
      const formBinding = formBindingData.createFormBindingData(newData);
      await formProvider.updateForm(formId, formBinding);
    } catch (error) {
      hilog.error(DOMAIN, TAG, `定时更新失败: ${(error as BusinessError).message}`);
    }
  }

  onRemoveForm(formId: string): void {
    // 清理定时器
    const timer = this.timers.get(formId);
    if (timer) {
      clearInterval(timer);
      this.timers.delete(formId);
    }
  }
}

🌐 五、分布式数据同步

HarmonyOS的分布式能力可以让服务卡片在多个设备间同步数据。

import { distributedData } from '@kit.DistributedDataKit';

class DistributedDataManager {
  private kvManager: distributedData.KVManager | null = null;
  private kvStore: distributedData.SingleKVStore | null = null;

  // 初始化分布式数据库
  async initDistributedKVStore(): Promise<void> {
    try {
      const context = getContext(this) as Context;
      const config: distributedData.Config = {
        bundleName: context.applicationInfo.name,
        userInfo: {
          userId: distributedData.UserType.SAME_USER_ID
        }
      };

      this.kvManager = distributedData.createKVManager(config);
      const options: distributedData.StoreConfig = {
        storeId: 'widget_data_store',
        kvStoreType: distributedData.KVStoreType.SINGLE_VERSION,
        securityLevel: distributedData.SecurityLevel.S2,
        autoSync: true
      };

      this.kvStore = await this.kvManager.getKVStore<distributedData.SingleKVStore>(options);
    } catch (error) {
      hilog.error(DOMAIN, TAG, `分布式数据库初始化失败: ${(error as BusinessError).message}`);
    }
  }

  // 同步卡片数据
  async syncCardData(formId: string, data: any): Promise<void> {
    if (!this.kvStore) return;

    try {
      await this.kvStore.put(`${formId}_data`, JSON.stringify(data));
      hilog.info(DOMAIN, TAG, '卡片数据已同步到分布式数据库');
    } catch (error) {
      hilog.error(DOMAIN, TAG, `数据同步失败: ${(error as BusinessError).message}`);
    }
  }

  // 获取同步的数据
  async getSyncedData(formId: string): Promise<any> {
    if (!this.kvStore) return null;

    try {
      const value = await this.kvStore.get(`${formId}_data`);
      return value ? JSON.parse(value.toString()) : null;
    } catch (error) {
      hilog.error(DOMAIN, TAG, `获取同步数据失败: ${(error as BusinessError).message}`);
      return null;
    }
  }
}

🎨 六、高级功能与交互

1. 多尺寸卡片适配

@Entry
@Component
struct AdaptiveWidgetCard {
  @LocalStorageProp('dimension') dimension: string = '2 * 4';
  @LocalStorageProp('data') data: any;

  build() {
    Column() {
      // 根据卡片尺寸调整布局
      if (this.dimension === '2 * 2') {
        this.buildSmallLayout();
      } else if (this.dimension === '2 * 4') {
        this.buildMediumLayout();
      } else if (this.dimension === '4 * 4') {
        this.buildLargeLayout();
      }
    }
  }

  @Builder
  buildSmallLayout() {
    Text(this.data.title)
      .fontSize(16)
      .margin(5);
    
    Text(this.data.value)
      .fontSize(20)
      .fontWeight(FontWeight.Bold);
  }

  @Builder
  buildMediumLayout() {
    Row() {
      Image(this.data.icon)
        .width(40)
        .height(40)
        .margin({ right: 10 })

      Column() {
        Text(this.data.title)
          .fontSize(16)
        Text(this.data.value)
          .fontSize(18)
          .fontWeight(FontWeight.Bold)
      }
    }
  }

  @Builder
  buildLargeLayout() {
    Column() {
      Text(this.data.title)
        .fontSize(18)
        .fontWeight(FontWeight.Bold)

      Divider()
        .margin(10)

      ForEach(this.data.items, (item: any) => {
        Row() {
          Text(item.label)
            .fontSize(14)
            .layoutWeight(1)
          Text(item.value)
            .fontSize(14)
            .fontColor(Color.Blue)
        }
        .width('100%')
        .margin({ bottom: 5 })
      })

      Divider()
        .margin(10)

      Text(`更新时间: ${this.data.updateTime}`)
        .fontSize(12)
        .fontColor(Color.Gray)
    }
  }
}

2. 动画与视觉效果

@Component
struct AnimatedWidgetCard {
  @State private isRefreshing: boolean = false;
  @State private rotation: number = 0;

  build() {
    Column() {
      // 刷新按钮带动画
      Row() {
        Image($r('app.media.refresh_icon'))
          .width(20)
          .height(20)
          .rotate({ angle: this.rotation })
          .onClick(() => this.startRefreshAnimation())

        Text('刷新数据')
          .fontSize(14)
          .margin({ left: 5 })
      }
      .margin(10)
      .onClick(() => this.handleRefresh())
    }
  }

  // 启动刷新动画
  private startRefreshAnimation(): void {
    this.isRefreshing = true;
    this.rotation = 0;
    
    animateTo({
      duration: 1000,
      iterations: -1, // 无限循环
      curve: Curve.Linear
    }, () => {
      this.rotation = 360;
    });
  }

  // 停止动画
  private stopRefreshAnimation(): void {
    this.isRefreshing = false;
    this.rotation = 0;
  }

  private async handleRefresh(): Promise<void> {
    this.startRefreshAnimation();
    
    try {
      // 模拟数据刷新
      await new Promise(resolve => setTimeout(resolve, 2000));
      this.stopRefreshAnimation();
    } catch (error) {
      this.stopRefreshAnimation();
    }
  }
}

📝 七、最佳实践与性能优化

1. 性能优化建议

  1. 数据量控制:单个卡片传输的数据不宜过大,建议控制在5KB以内。
  2. 更新频率:合理设置更新间隔,避免频繁刷新影响性能。
  3. 内存管理:及时清理不用的资源和监听器。
  4. 图片优化:使用适当尺寸的图片资源,避免内存占用过大。

2. 开发注意事项

  • 卡片约束:ArkTS卡片仅支持导入标识"支持在ArkTS卡片中使用"的模块,不支持导入共享包,不支持使用native(C++)语言开发。
  • 事件处理:卡片的事件处理和使用方的事件处理是独立的,建议在使用方支持左右滑动的场景下卡片内容不要使用左右滑动功能的组件,以防手势冲突影响交互体验。
  • 调试限制:暂不支持断点调试能力、Hot Reload热重载、setTimeOut/ setInterval()。

3. 错误处理与健壮性

class ErrorHandler {
  static handleCardError(error: BusinessError, context: string): void {
    hilog.error(DOMAIN, TAG, `卡片错误 [${context}]: ${error.message}`);
    
    // 根据错误类型采取不同策略
    if (error.code === 1001) { // 假设1001是网络错误
      this.showNetworkError();
    } else if (error.code === 1002) { // 数据格式错误
      this.showDataError();
    }
  }

  static showNetworkError(): void {
    // 显示网络错误提示
    postCardAction(this, {
      action: 'message',
      params: JSON.stringify({
        action: 'showError',
        message: '网络连接失败,请检查网络设置'
      })
    });
  }

  static showDataError(): void {
    // 显示数据错误提示
    postCardAction(this, {
      action: 'message',
      params: JSON.stringify({
        action: 'showError',
        message: '数据格式错误,请尝试刷新'
      })
    });
  }
}

通过以上完整的实现方案,你可以在HarmonyOS 5.0+中开发出功能丰富、性能优异的动态服务卡片。记得在实际开发中根据具体需求进行调整和优化。

需要参加鸿蒙认证的请点击 鸿蒙认证链接

posted @ 2025-09-24 16:39  猫林老师  阅读(41)  评论(0)    收藏  举报