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的数据变化。这种架构能够有效保证卡片和应用间的数据实时同步。

