如何在卡片和应用间共享UI状态
===============

#### 问题现象

以计数器为例，在应用内和卡片中的计数可以同步增加或减少。如何在卡片和应用间共享UI状态？

#### 背景知识

*   [Form Kit（卡片开发服务）](https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/formkit-overview)提供了一种在桌面、锁屏等系统应用上嵌入显示应用信息的开发框架和API，可以将应用内用户关注的重要信息或常用操作抽取到服务卡片（简称“卡片”）上，通过将卡片添加到桌面、锁屏等系统应用上，以达到信息展示、服务直达的便捷体验效果。
*   [@ohos.commonEventManager (公共事件模块)](https://developer.huawei.com/consumer/cn/doc/harmonyos-references/js-apis-commoneventmanager)提供了公共事件相关的能力，包括发布公共事件、订阅公共事件、以及退订公共事件。
*   [formProvider.getPublishedRunningFormInfos](https://developer.huawei.com/consumer/cn/doc/harmonyos-references/js-apis-app-form-formprovider#formprovidergetpublishedrunningforminfos20)获取设备上当前应用程序所有已经加载到桌面的卡片信息。
*   [formProvider.updateForm](https://developer.huawei.com/consumer/cn/doc/harmonyos-references/js-apis-app-form-formprovider#formproviderupdateform)更新指定的卡片。

#### 解决方案

**解决方案一：**

由于卡片与应用属于不同进程，所以选择使用commonEventManager公共事件模块，分别在主应用端和卡片端发布、订阅公共事件，实现UI状态共享。

1.  创建src/main/ets/utils/SubscriberClass.ets，将其作为工具类，提供订阅和发布公共事件方法。
    
    import commonEventManager from '@ohos.commonEventManager';
    
    
    class SubscriberClass {
      subscriber?: commonEventManager.CommonEventSubscriber;
      publishCount: number = 0;
    
    
      publish(eventType: string, data: string = '') {
        commonEventManager.publish(eventType, { data }, () => {
          console.error(\`发布失败\`);
        });
      }
    
    
      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();
    
2.  在主应用的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());
            });
        }.width('100%').height('100%').justifyContent(FlexAlign.Center);
      }
    }
    
3.  在卡片生命周期EntryFormAbility的onAddForm生命周期和onFormEvent生命周期分别订阅和发布公共事件。
    
    import { formBindingData, FormExtensionAbility, formInfo, formProvider } from '@kit.FormKit';
    import { Want } from '@kit.AbilityKit';
    import { subscriberClass } from '../utils/SubscriberClass';
    
    
    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._
        console.info('onCastToNormalForm', formId);
      }
    
    
      onUpdateForm(formId: string) {
      _  // Called to notify the form provider to update a specified form._
        console.info('onUpdateForm', formId);
      }
    
    
      onFormEvent(formId: string, message: string) {
       _ // Called when a specified message event defined by the form provider is triggered._
        console.info('onFormEvent', formId);
        // 发布公共事件
        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._
        console.info('onRemoveForm', formId);
      }
    
    
      onAcquireFormState(want: Want) {
        console.info('onAcquireFormState', want.bundleName);
        return formInfo.FormState.READY;
      }
    };
    
4.  在卡片页面接收来自应用的数据，并通过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](https://developer.huawei.com/consumer/cn/doc/harmonyos-references/js-apis-app-form-formprovider#formprovidergetpublishedrunningforminfos20)接口首先获取设备上当前应用程序所有已经加载到桌面的卡片信息，然后通过[formProvider.updateForm](https://developer.huawei.com/consumer/cn/doc/harmonyos-references/js-apis-app-form-formprovider#formproviderupdateform)接口对指定的卡片进行更新；

当卡片需要将数据同步至主应用时，与方案一致，通过[postCardAction](https://developer.huawei.com/consumer/cn/doc/harmonyos-references/js-apis-postcardaction)的message事件触发卡片onFormEvent生命周期，然后发布公共事件。

1.  创建src/main/ets/utils/SubscriberClass.ets，并将其作为工具类。
    
    import commonEventManager from '@ohos.commonEventManager';
    
    
    class SubscriberClass {
      subscriber?: commonEventManager.CommonEventSubscriber;
      publishCount: number = 0;
    
    
      publish(eventType: string, data: string = '') {
        commonEventManager.publish(eventType, { data }, () => {
          console.error(\`发布失败\`);
        });
      }
    
    
      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();
    
2.  在主应用的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('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++;
            });
        }.width('100%').height('100%').justifyContent(FlexAlign.Center);
      }
    
    
      _// 计数器变化监听_
      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\`);
          });
        });
      }
    }
    
3.  在卡片生命周期EntryFormAbility的onFormEvent生命周期发布公共事件，无需像步骤一那样订阅发布事件。
    
    import { formBindingData, FormExtensionAbility, formInfo } from '@kit.FormKit';
    import { Want } from '@kit.AbilityKit';
    import { subscriberClass } from '../utils/SubscriberClass';
    
    
    interface Param {
      count: number,
      params: string,
      action: string
    }
    
    
    
    
    export default class EntryFormAbility extends FormExtensionAbility {
      onAddForm(want: Want) {
       _ // Called to return a FormBindingData object._
        let formId: string = want.parameters!\[formInfo.FormParam.IDENTITY\_KEY\] as string;
        console.info('onAddForm', formId);
        const formData = '';
        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._
        console.info('onCastToNormalForm', formId);
      }
    
    
      onUpdateForm(formId: string) {
       _ // Called to notify the form provider to update a specified form._
        console.info('onUpdateForm', formId);
      }
    
    
      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._
        console.info('onRemoveForm', formId);
      }
    
    
      onAcquireFormState(want: Want) {
      _  // Called to return a {@link FormState} object._
        console.info('onAcquireFormState', want.bundleName);
        return formInfo.FormState.READY;
      }
    };
    
4.  在卡片页面接收来自应用的数据，并通过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);
      }
    }
    

#### 常见FAQ

Q：通过AppStorage为何无法在应用与卡片间共享UI状态？

A：AppStorage支持应用主线程中多个UIAbility实例之间的状态共享。AppStorage是与UI相关的数据，必须在UI线程中运行，无法与其他线程共享。卡片运行在独立进程（FormExtensionAbility），与主应用进程内存隔离，直接访问AppStorage会导致数据获取失败或同步异常。

Q：使用公共事件模块实现卡片和应用间共享UI状态有什么缺陷？

A：FormExtensionAbility创建后10秒内无操作将会被清理。如果FormExtensionAbility被清理后，从主应用更新卡片将单向失败，而卡片更新主应用仍可正常进行。

#### 总结

*   解决方案一通过公共事件模块实现卡片和应用间共享UI状态，该方法在主应用和卡片间实现方式统一，方案简洁，但是当卡片进程消亡后，主应用无法再同步数据至卡片。
*   解决方案二为了对方案一进行改进，在主应用同步数据至卡片时采用调用卡片更新接口的方式进行实现。而该方案当主应用从任务列表清除再冷启动时，无法同步卡片最后更新的数据，需要考虑采用持久化方案。