HarmonyOS 鸿蒙Next中关于卡片与应用间的数据共享问题
HarmonyOS 鸿蒙Next中关于卡片与应用间的数据共享问题
1.问题
现设想一个加减计数器卡片,应用内也有计数器;这两个计数器如何进行数据的共享同步,即 卡片操作加减后的数值和点进应用计数器的数值是一样的?
2.相关文件
// 卡片
// CountCard.ets
/**
 * @file: CountCard.ets
 * @author: Yeluzii
 * @date: 2025/10/15
 */
import { hilog } from '@kit.PerformanceAnalysisKit';
import { CountProvider } from '../../providers/CountProvider';
const TAG: string = 'CountCard';
const DOMAIN: number = 0x0005;
const COUNT_KEY: string = 'counter_value';
let storageUpdateCall = new LocalStorage();
@Entry(storageUpdateCall)
@Component
struct CountCard {
  @LocalStorageProp('formId') formId: string = '12400633174999288';
  @LocalStorageProp('counter_value') count: number = 0;
  private countProvider: CountProvider = CountProvider.getInstance();
  aboutToAppear() {
    // 初始化计数器数据
    this.loadCountData();
    // 添加监听器以同步数据更新
    this.countProvider.addListener(() => {
      this.count = this.countProvider.getCount();
    });
  }
  // 加载计数器数据
  private async loadCountData(): Promise<void> {
    try {
      await this.countProvider.init();
      this.count = this.countProvider.getCount();
    } catch (error) {
      console.error('加载计数器数据失败:', error);
    }
  }
  // 增加计数
  private async increment(): Promise<void> {
    await this.countProvider.increment();
    this.count = this.countProvider.getCount()
  }
  // 减少计数
  private async decrement(): Promise<void> {
    await this.countProvider.decrement();
    this.count = this.countProvider.getCount()
  }
  // 重置计数
  private async reset(): Promise<void> {
    await this.countProvider.reset();
    this.count = this.countProvider.getCount()
  }
  build() {
    Stack() {
      // 主要内容区域
      Column({ space: 16 }) {
        // 标题
        Text("计数器")
          .fontSize(20)
          .fontWeight(FontWeight.Medium)
          .fontColor($r('app.color.text_primary'))
          .width('100%')
          .textAlign(TextAlign.Center)
        // 计数显示(大字体)
        Text(`${this.count}`)
          .fontSize(72)
          .fontWeight(FontWeight.Bold)
          .fontColor($r('app.color.primary_blue'))
          .width('100%')
          .textAlign(TextAlign.Center)
          .margin({ top: 8 })
        // 操作按钮
        Row({ space: 20 }) {
          Button('-')
            .type(ButtonType.Normal)
            .width(50)
            .height(50)
            .fontSize(28)
            .backgroundColor($r('app.color.primary_blue'))
            .fontColor(Color.White)
            .borderRadius(25)
            .onClick(() => {
              this.decrement();
            })
          Button('重置')
            .type(ButtonType.Normal)
            .width(70)
            .height(50)
            .fontSize(16)
            .backgroundColor($r('app.color.primary_blue'))
            .fontColor(Color.White)
            .borderRadius(8)
            .onClick(() => {
              this.reset();
            })
          Button('+')
            .type(ButtonType.Normal)
            .width(50)
            .height(50)
            .fontSize(28)
            .backgroundColor($r('app.color.primary_blue'))
            .fontColor(Color.White)
            .borderRadius(25)
            .onClick(() => {
              this.increment()
            })
        }
        .width('100%')
        .justifyContent(FlexAlign.Center)
        .margin({ top: 16 })
      }
      .width('100%')
      .height('100%')
      .padding(24)
      .justifyContent(FlexAlign.Center)
      .backgroundColor($r('app.color.card_background'))
      .borderRadius(16)
      .shadow({
        radius: 8,
        color: $r('app.color.shadow'),
        offsetX: 0,
        offsetY: 2
      })
    }
    .width('100%')
    .height('100%')
  }
}
// CountFormAbility.ets
import { formBindingData, formInfo, formProvider, FormExtensionAbility } from '@kit.FormKit';
import type { Want } from '@kit.AbilityKit';
import { hilog } from '@kit.PerformanceAnalysisKit';
import { BusinessError } from '@kit.BasicServicesKit';
const TAG: string = 'CountFormAbility';
const DOMAIN: number = 0x0006;
const COUNT_KEY: string = 'counter_value';
export interface FormData {
  counter_value: number
}
export default class CountFormAbility extends FormExtensionAbility {
  onAddForm(want: Want) {
    const formId: string = want.parameters?.['ohos.extra.param.key.form_identity'] as string;
    hilog.info(DOMAIN, TAG, "计数器卡片创建 " + `formId: ${formId}`)
    //
    // // 获取当前计数器值并传递给卡片
    // this.updateFormContent(formId);
    //
    // return formBindingData.createFormBindingData({});
    // 从 AppStorage 获取当前计数器值
    let counterValue = AppStorage.get<number>('counter_value') || 0;
    // 构建卡片显示的数据
    let formData: FormData = {
      counter_value: counterValue
    }
    // 更新卡片
    return formBindingData.createFormBindingData(formData)
  }
  onUpdateForm(formId: string) {
    // 更新卡片数据
    this.updateFormContent(formId);
    let formProviderData = formBindingData.createFormBindingData({});
    return formProviderData;
  }
  private updateFormContent(formId: string) {
    try {
      // 从AppStorage读取计数器值
      const count: number = AppStorage.get<number>('counter_value') || 0;
      // 更新卡片显示内容
      const formProviderData = formBindingData.createFormBindingData({
        count_value: count
      });
      formProvider.updateForm(formId, formProviderData);
    } catch (error) {
      const businessError = error as BusinessError;
      hilog.error(DOMAIN, TAG, `更新卡片内容失败: ${businessError.message}`);
    }
  }
  onRemoveForm(formId: string) {
    // 移除卡片
    hilog.info(DOMAIN, TAG, "计数器卡片删除: " + formId)
  }
  onAcquireFormState(want: Want) {
    // 获取卡片状态
    return formInfo.FormState.READY;
  }
};
// CountProvider.ets
import { hilog } from '@kit.PerformanceAnalysisKit';
import { BusinessError } from '@kit.BasicServicesKit';
const TAG: string = 'CountProvider';
const DOMAIN: number = 0x0004;
/**
 * 计数器数据提供者
 * 提供计数器相关操作和数据同步功能
 */
class CountProvider {
  private static instance: CountProvider | null = null;
  private count: number = 0;
  private listeners: Array<() => void> = [];
  // 私有构造函数,实现单例模式
  private constructor() {}
  /**
   * 获取CountProvider单例实例
   */
  static getInstance(): CountProvider {
    if (!CountProvider.instance) {
      CountProvider.instance = new CountProvider();
    }
    return CountProvider.instance;
  }
  /**
   * 初始化计数器数据
   */
  async init(): Promise<void> {
    try {
      // 从AppStorage读取计数器值
      const storedCount = AppStorage.get<number>('counter_value');
      if (storedCount !== undefined) {
        this.count = storedCount;
      } else {
        // 如果AppStorage中没有值,则初始化为0
        AppStorage.setOrCreate<number>('counter_value', 0);
      }
      hilog.info(DOMAIN, TAG, `计数器初始化完成,当前值: ${this.count}`);
    } catch (error) {
      const businessError = error as BusinessError;
      hilog.error(DOMAIN, TAG, `初始化计数器失败: ${businessError.message}`);
    }
  }
  // 更新数值
   updateCounter(count: number): void {
    this.count = count
    AppStorage.set<number>('counter_value', count);
  }
  /**
   * 获取当前计数
   */
  getCount(): number {
    return this.count;
  }
  /**
   * 增加计数
   */
  async increment(): Promise<void> {
    this.count++;
    await this.saveCount();
    this.notifyListeners();
  }
  /**
   * 减少计数
   */
  async decrement(): Promise<void> {
    if (this.count > 0) {
      this.count--;
      await this.saveCount();
      this.notifyListeners();
    }
  }
  /**
   * 重置计数
   */
  async reset(): Promise<void> {
    this.count = 0;
    await this.saveCount();
    this.notifyListeners();
  }
  /**
   * 保存计数到本地存储
   */
  private async saveCount(): Promise<void> {
    try {
      AppStorage.set<number>('counter_value', this.count);
      hilog.info(DOMAIN, TAG, `计数器保存成功,当前值: ${this.count}`);
    } catch (error) {
      const businessError = error as BusinessError;
      hilog.error(DOMAIN, TAG, `保存计数器失败: ${businessError.message}`);
    }
  }
  /**
   * 添加数据变化监听器
   */
  addListener(listener: () => void): void {
    if (!this.listeners.includes(listener)) {
      this.listeners.push(listener);
    }
  }
  /**
   * 移除数据变化监听器
   */
  removeListener(listener: () => void): void {
    const index = this.listeners.indexOf(listener);
    if (index > -1) {
      this.listeners.splice(index, 1);
    }
  }
  /**
   * 通知所有监听器数据已更新
   */
  private notifyListeners(): void {
    this.listeners.forEach(listener => {
      try {
        listener();
      } catch (error) {
        hilog.error(DOMAIN, TAG, `通知监听器失败: ${error}`);
      }
    });
  }
}
// 导出单例
export { CountProvider };
// Index.ets
import { hilog } from '@kit.PerformanceAnalysisKit';
import { CountProvider } from '../providers/CountProvider';
const TAG: string = 'Index';
const DOMAIN: number = 0x0002;
/**
 * 时钟卡片应用主页面
 */
@Entry
@Component
struct Index {
  @State count: number = 0
  private countProvider: CountProvider = CountProvider.getInstance();
  // 组件生命周期:组件出现时启动定时器
  aboutToAppear() {
    // 初始化计数器数据
    this.initCountData();
  }
  // 初始化计数器数据
  private async initCountData(): Promise<void> {
    try {
      await this.countProvider.init();
      this.loadCountData();
      // 添加监听器以同步数据更新
      this.countProvider.addListener(() => {
        this.count = this.countProvider.getCount();
      });
      // 初始化AppStorage中的计数器值
      this.count = AppStorage.get('counter_value')!
    } catch (error) {
    }
  }
  // 加载计数器数据
  private loadCountData(): void {
    try {
      this.count = this.countProvider.getCount();
    } catch (error) {
      console.error('加载计数器数据失败:', error);
    }
  }
  // 增加计数
  private async increment(): Promise<void> {
    await this.countProvider.increment();
    this.count = this.countProvider.getCount()
  }
  // 减少计数
  private async decrement(): Promise<void> {
    await this.countProvider.decrement();
    this.count = this.countProvider.getCount()
  }
  // 重置计数
  private async reset(): Promise<void> {
    await this.countProvider.reset();
    this.count = this.countProvider.getCount()
  }
  build() {
    Scroll() {
      Column() {
        // 计数器区域
        Column({ space: 10 }) {
          Text("计数器")
            .fontSize(24)
            .width('100%')
            .textAlign(TextAlign.Center)
          Text(`${this.count}`)
            .fontSize(48)
            .fontWeight(FontWeight.Bold)
            .width('100%')
            .textAlign(TextAlign.Center)
          Row({ space: 20 }) {
            Button('-')
              .type(ButtonType.Normal)
              .width(45)
              .height(45)
              .fontSize(24)
              .backgroundColor($r('app.color.primary_blue'))
              .fontColor(Color.White)
              .onClick(() => {
                this.decrement();
              })
            Button('重置')
              .type(ButtonType.Normal)
              .width(60)
              .height(40)
              .fontSize(14)
              .backgroundColor($r('app.color.primary_blue'))
              .fontColor(Color.White)
              .onClick(() => {
                this.reset();
              })
            Button('+')
              .type(ButtonType.Normal)
              .width(45)
              .height(45)
              .fontSize(24)
              .backgroundColor($r('app.color.primary_blue'))
              .fontColor(Color.White)
              .onClick(() => {
                this.increment();
              })
          }
          .width('100%')
          .justifyContent(FlexAlign.Center)
        }
        .padding(20)
        .border({
          radius: 16,
          width: 1,
          color: Color.Black,
        })
        .shadow({
          radius: 5,
          offsetX: 5,
          offsetY: 5,
          color: Color.Gray
        })
        .margin({
          top: 20
        })
        .width('90%')
      }
      .width('100%')
    }
  }
}
更多关于HarmonyOS 鸿蒙Next中关于卡片与应用间的数据共享问题的实战教程也可以访问 https://www.itying.com/category-93-b0.html
【背景知识】
- Form Kit(卡片开发服务)提供了一种在桌面、锁屏等系统应用上嵌入显示应用信息的开发框架和API,可以将应用内用户关注的重要信息或常用操作抽取到服务卡片(简称“卡片”)上,通过将卡片添加到桌面、锁屏等系统应用上,以达到信息展示、服务直达的便捷体验效果。
- [@ohos.commonEventManager (公共事件模块)](https://developer.huawei.com/consumer/cn/doc/harmonyos-references/js-apis-commoneventmanager)提供了公共事件相关的能力,包括发布公共事件、订阅公共事件、以及退订公共事件。
- formProvider.getPublishedRunningFormInfos获取设备上当前应用程序所有已经加桌的卡片信息。
- formProvider.updateForm更新指定的卡片。
【解决方案】
解决方案一: 由于卡片与应用属于不同进程,所以选择使用commonEventManager公共事件模块,分别在主应用端和卡片端发布、订阅公共事件,实现UI状态共享。
- 创建src/main/ets/utils/SubscriberClass.ets,将其作为工具类,提供订阅和发布公共事件方法。
import commonEventManager from '[@ohos](/user/ohos).commonEventManager'
class SubscriberClass {
  subscriber?: commonEventManager.CommonEventSubscriber
  publishCount: number = 0
  publish(eventType: string, data: string = '') {
    commonEventManager.publish(eventType, { data }, (err) => {
      console.error(`发布失败: ${err.message}`)
    })
  }
  subscribe(eventType: string, callback: (event: string) => void) {
    // 1.创建订阅者
    commonEventManager.createSubscriber({ events: [eventType] }, (err, data) => {
      if (err) {
        return console.info('logData:', '创建订阅者失败')
      }
      // 2.data是订阅者
      this.subscriber = data
      if (this.subscriber) {
        // 3.订阅事件
        commonEventManager.subscribe(this.subscriber, (err, data) => {
          if (err) {
            return console.info('logData:', '订阅者事件失败')
          }
          if (data.data) {
            callback(data.data)
          }
        })
      }
    })
  }
}
export const subscriberClass = new SubscriberClass()
- 在主应用的aboutToAppear方法中订阅来自卡片发布的事件,同时分别在新增和减少按钮的点击事件中发布应用内数量变更事件。
import { subscriberClass } from '../utils/SubscriberClass'
@Component
@Entry
struct Index {
  @State count: number = 0
  aboutToAppear(): void {
    // 订阅来自卡片发布的事件
    subscriberClass.subscribe('appUpdate', (event) => {
      this.count = Number(event)
    })
  }
  build() {
    Row({ space: 10 }) {
      Button('-')
        .width(80)
        .onClick(() => {
          // 减少计数器
          this.count--
          // 发布事件
          subscriberClass.publish('cardUpdate', this.count.toString())
        })
      Text(this.count.toString())
      Button('+')
        .width(80)
        .onClick(() => {
          // 增加计数器
          this.count++
          // 发布事件
          subscriberClass.publish('cardUpdate', this.count.toString())
        })
    }
  }
}
- 在卡片生命周期EntryFormAbility的onAddForm生命周期和onFormEvent生命周期分别订阅和发布公共事件。
interface Param {
  count: number,
  params: string,
  action: string
}
export default class EntryFormAbility extends FormExtensionAbility {
  onAddForm(want: Want) {
    // Called to return a FormBindingData object.
    let formData = '';
    let formId: string = want.parameters![formInfo.FormParam.IDENTITY_KEY] as string
    // 订阅公共事件
    subscriberClass.subscribe('cardUpdate', (event) => {
      formProvider.updateForm(formId, formBindingData.createFormBindingData({
        count: event
      })).catch(() => {
        console.error('卡片更新失败了')
      })
    })
    return formBindingData.createFormBindingData(formData);
  }
  onCastToNormalForm(formId: string) {
    // Called when the form provider is notified that a temporary form is successfully
    // converted to a normal form.
  }
  onUpdateForm(formId: string) {
    // Called to notify the form provider to update a specified form.
  }
  onFormEvent(formId: string, message: string) {
    // Called when a specified message event defined by the form provider is triggered.
    // 发布公共事件
    let param = JSON.parse(message) as Param
    subscriberClass.publish('appUpdate', param.count.toString())
  }
  onRemoveForm(formId: string) {
    // Called to notify the form provider that a specified form has been destroyed.
  }
  onAcquireFormState(want: Want) {
    return formInfo.FormState.READY;
  }
}
- 在卡片页面接收来自应用的数据,并通过postCardAction事件发布计数器数据变化。
const localStorage = new LocalStorage()
@Entry(localStorage)
@Component
struct WidgetCard {
  @LocalStorageProp('count')
  count: number = 0
  build() {
    Column() {
      Row({ space: 10 }) {
        Button('-')
          .width(80)
          .onClick(() => {
            this.count--
            postCardAction(this, {
              'action': 'message',
              params: {
                count: this.count
              }
            })
          })
        Text(this.count.toString())
        Button('+')
          .width(80)
          .onClick(() => {
            this.count++
            postCardAction(this, {
              'action': 'message',
              params: {
                count: this.count
              }
            })
          })
      }
    }
    .width('100%')
    .height('100%')
    .justifyContent(FlexAlign.Center)
  }
}
解决方案二: 当主应用需要将数据同步至卡片时,在主应用中可通过formProvider.getPublishedRunningFormInfos接口首先获取设备上当前应用程序所有已经加桌的卡片信息,然后通过formProvider.updateForm接口对指定的卡片进行更新。当卡片需要将数据同步至主应用时,与方案一一致,通过postCardAction的message事件触发卡片onFormEvent生命周期,然后发布公共事件。
- 参见方案一步骤一,创建src/main/ets/utils/SubscriberClass.ets,并将其作为工具类。
- 在主应用的aboutToAppear方法中订阅来自卡片发布的事件,同时获取当前设备当前应用已上桌的卡片,过滤出需要更新的卡片信息。对计数器的值使用@Watch进行监听,当发送变化时,通过updateForm方法更新卡片。
import { subscriberClass } from '../utils/SubscriberClass'
import { formBindingData, formInfo, formProvider } from '@kit.FormKit'
import { BusinessError } from '@kit.BasicServicesKit'
@Component
@Entry
struct Index {
  [@Watch](/user/Watch)('countChange') @State count: number = 0
  updateForms: formInfo.RunningFormInfo[] = []
  aboutToAppear(): void {
    // 订阅来自卡片发布的事件
    subscriberClass.subscribe('appUpdate', (event) => {
      this.count = Number(event)
    })
    // 获取已上桌的卡片
    formProvider.getPublishedRunningFormInfos().then((data: formInfo.RunningFormInfo[]) => {
      console.info(`formProvider getPublishedRunningFormInfos, data: ${JSON.stringify(data)}`);
      // 过滤出需要同步更新的卡片,可以根据卡片名称
      this.updateForms = data.filter((item) => item.formName === 'widget')
    }).catch((error: BusinessError) => {
      console.error(`promise error, code: ${error.code}, message: ${error.message})`);
    });
  }
  build() {
    Row({ space: 10 }) {
      Button('-')
        .width(80)
        .onClick(() => {
          // 减少计数器
          this.count--
        })
      Text(this.count.toString())
      Button('+')
        .width(80)
        .onClick(() => {
          // 增加计数器
          this.count++
        })
    }
  }
  // 计数器变化监听
  countChange() {
    // 遍历卡片
    this.updateForms.forEach((item) => {
      // 值更新
      formProvider.updateForm(item.formId, formBindingData.createFormBindingData({
        count: this.count
      }), (error: BusinessError) => {
        if (error) {
          console.error(`formProvider updateForm callback error, code: ${error.code}, message: ${error.message})`);
          return;
        }
        console.info(`formProvider updateForm success`);
      })
    })
  }
}
- 在卡片生命周期EntryFormAbility的onFormEvent生命周期发布公共事件,无需要像步骤一那样订阅发布事件。
onFormEvent(formId: string, message: string) {
  // Called when a specified message event defined by the form provider is triggered.
  let param = JSON.parse(message) as Param
  subscriberClass.publish('appUpdate', param.count.toString())
}
- 参见方案一步骤四,在卡片页面接收来自应用的数据,并通过postCardAction事件发布计数器数据变化。
【常见FAQ】
Q:通过AppStorage为何无法在应用与卡片间共享UI状态? A:AppStorage支持应用主线程中多个UIAbility实例之间的状态共享。AppStorage是与UI相关的数据,必须在UI线程中运行,无法与其他线程共享。卡片运行在独立进程(FormExtensionAbility),与主应用进程内存隔离,直接访问AppStorage会导致数据获取失败或同步异常。
Q:使用公共事件模块实现卡片和应用间共享UI状态有什么缺陷? A:FormExtensionAbility创建后10秒内无操作将会被清理。如果FormExtensionAbility被清理后,从主应用更新卡片将单向失败,而卡片更新主应用仍可正常进行。
【总结】
- 解决方案一通过公共事件模块实现卡片和应用间共享UI状态,该方法在主应用和卡片间实现方式统一,方案简洁,但是当卡片进程消亡后,主应用无法再同步数据至卡片。
- 解决方案二为了对方案一进行改进,在主应用同步数据至卡片时采用调用卡片更新接口的方式进行实现。而该方案当主应用从任务列表清除再冷启动时,无法同步卡片最后更新的数据,需要考虑采用持久化方案。
更多关于HarmonyOS 鸿蒙Next中关于卡片与应用间的数据共享问题的实战系列教程也可以访问 https://www.itying.com/category-93-b0.html
在HarmonyOS Next中,卡片与应用间的数据共享主要通过ArkUI的UIAbility和FormExtensionAbility实现。数据传递依赖Want和FormBindingData对象,应用通过FormProvider将数据更新至卡片。共享存储使用轻量级数据存储或分布式数据对象,支持跨设备同步。开发者需在配置文件中声明卡片权限和数据共享路径,确保数据交互安全高效。
在HarmonyOS Next中实现卡片与应用间的数据共享,可以通过AppStorage结合事件监听机制来完成。从你的代码来看,整体架构设计合理,但有几个关键点需要注意:
- 
数据同步机制:你使用了AppStorage作为共享数据源,CountProvider作为数据管理层,这是正确的做法。AppStorage提供了应用级的状态共享,适合卡片和应用之间的数据同步。 
- 
卡片数据更新:在CountFormAbility中,onUpdateForm方法需要正确实现数据更新逻辑。目前你的代码中,onUpdateForm返回了空的formProviderData,应该调用updateFormContent方法: 
onUpdateForm(formId: string) {
  this.updateFormContent(formId);
}
- 
监听器管理:在CountProvider中,建议在组件销毁时移除监听器,避免内存泄漏。可以在卡片的aboutToDisappear生命周期中调用removeListener。 
- 
数据一致性:确保所有数据操作都通过CountProvider进行,避免直接操作AppStorage,这样可以保证数据变更时能正确通知所有监听器。 
- 
卡片初始化:在CountFormAbility的onAddForm中,你已经正确地从AppStorage获取初始值,这保证了卡片创建时就能显示正确的计数。 
当前实现的核心是通过AppStorage存储共享数据,CountProvider管理数据操作和变更通知,卡片和应用都监听CountProvider的数据变化。这种架构能够有效保证卡片和应用间的数据实时同步。
 
        
       
                   
                   
                  

