DEV Community

HarmonyOS
HarmonyOS

Posted on

How to implement two-way data synchronization between the app and the widget?

Read the original article:How to implement two-way data synchronization between the app and the widget?

Requirement Description

When users add widgets, the data should be in sync between the app and the widget.

Background Knowledge

  • There may be cases where you want to provide in a widget access to features available in your application running in the foreground, for example, the play, pause, and stop buttons in a music application widget. This is where the call capability of the postCardAction API comes in handy. This capability, when used in a widget, can start the specified UIAbility of the widget provider in the background. It also allows the widget to call the specified method of the application and transfer data so that the application, while in the background, can behave accordingly in response to touching the buttons on the widget.
  • The widget provider can call updateForm to actively update the widget.

Implementation Steps

Step 1: Create an application.

Step 2: Define data models.

Step 3: Implement preferences to store data.

Step 4: Create a widget with dimensions '2*3'

Step 5: Modify MyModeFormAbility to store widget IDs.

Step 6: Modify Entrybility

Step 7: Modify Index.ets to show current mode.

Step 8: Add a widget and see two-way synchronization.

Code Snippet

Step 1: Create an application.

Step 2: Define data models.

  • Mode (Basic enumeration data)
// model/Mode.ets

export enum Mode {
  sad,
  neutral,
  happy
}
Enter fullscreen mode Exit fullscreen mode
  • ModeData and ModeParcelable (For two-way data sync)
// model/ModeParcelable.ets

import { Mode } from "./Mode";
import { rpc } from "@kit.IPCKit";

export interface ModeData {
  mode: Mode
}

export class ModeParcelable implements rpc.Parcelable {
  mode: Mode

  constructor(mode: Mode) {
    this.mode = mode
  }

  marshalling(messageSequence: rpc.MessageSequence): boolean {
    messageSequence.writeInt(this.mode)
    return true;
  }

  unmarshalling(messageSequence: rpc.MessageSequence): boolean {
    const dataObj: ModeData = JSON.parse(messageSequence.readString())
    this.mode = dataObj.mode
    return true;
  }
}
Enter fullscreen mode Exit fullscreen mode

Step 3: Implement preferences to store data.

  • WidgetIdPrefs (To store widget ids)
// util/WidgetIdPrefs.ets

import { Context } from '@kit.AbilityKit';
import { preferences } from '@kit.ArkData';

export default class WidgetIdPrefs {
  static addFormID(context: Context, formID: string) {
    console.log('addFormID', formID);
    let prefs = preferences.getPreferencesSync(context, { name: 'ids' });
    prefs.putSync(formID, formID)
    prefs.flushSync()
  }

  static removeFormID(context: Context, formID: string) {
    console.log('removeFormID', formID);
    let prefs = preferences.getPreferencesSync(context, { name: 'ids' });
    prefs.deleteSync(formID)
    prefs.flushSync()
  }

  static getFormIDs(context: Context): string[] {
    console.log('getFormIDs');
    let prefs = preferences.getPreferencesSync(context, { name: 'ids' });
    return Object.values(prefs.getAllSync())
  }
}
Enter fullscreen mode Exit fullscreen mode
  • ModePrefs (to store current mode)
// util/ModePrefs.ets

import { Context } from '@kit.AbilityKit';
import { preferences } from '@kit.ArkData';
import { Mode } from '../model/Mode';

export default class ModePrefs {
  static setMode(context: Context, newMode: Mode) {
    let prefs = preferences.getPreferencesSync(context, { name: 'mode_data' });
    prefs.putSync('mode', newMode)
    prefs.flushSync()
  }

  static getMode(context: Context): Mode {
    let prefs = preferences.getPreferencesSync(context, { name: 'mode_data' });
    return prefs.getSync('mode', Mode.happy) as Mode
  }
}
Enter fullscreen mode Exit fullscreen mode

Step 4: Create a widget with dimensions '2*3' (This can be added to the cards page of the watch) and name it MyMode. For details about how to create a widget, please visit: Creating an ArkTS Widget

  • MyModeCard
// mymode/pages/MyModeCard.ets

import { Mode } from '../../model/Mode'

let ls = new LocalStorage();

@Entry(ls)
@Component
struct MyModeCard {
  @LocalStorageProp('mode') mode: Mode = Mode.happy

  build() {
    Row({ space: 8 }) {
      Button('😭')
        .fontSize(18)
        .backgroundColor(this.mode === Mode.sad ? Color.Red : Color.Grey)
        .type(ButtonType.Circle)
        .layoutWeight(1)
        .onClick(() => {
          this.changeMode(Mode.sad)
        })
      Button('😑')
        .fontSize(18)
        .backgroundColor(this.mode === Mode.neutral ? Color.Yellow : Color.Grey)
        .type(ButtonType.Circle)
        .layoutWeight(1)
        .onClick(() => {
          this.changeMode(Mode.neutral)
        })
      Button('😊')
        .fontSize(18)
        .backgroundColor(this.mode === Mode.happy ? Color.Green : Color.Grey)
        .type(ButtonType.Circle)
        .layoutWeight(1)
        .onClick(() => {
          this.changeMode(Mode.happy)
        })
    }
    .padding(8)
    .height('100%')
    .backgroundColor($r('sys.color.comp_background_primary'))
    .onClick(() => {
      postCardAction(this, {
        action: 'router',
        abilityName: 'EntryAbility',
      });
    })
  }

  changeMode(newMode: Mode) {
    if (this.mode !== newMode) {
      this.mode = newMode
      this.postCardActionCall(newMode)
    }
  }

  // call action to wake app
  postCardActionCall(newMode: Mode) {
    postCardAction(this, {
      action: 'call',
      abilityName: 'EntryAbility',
      params: {
        // function name
        method: 'changeMode',
        // additional parameters
        mode: newMode
      }
    });
  }
}
Enter fullscreen mode Exit fullscreen mode

Step 5: Modify MyModeFormAbility to store widget IDs.

  • MyModeFormAbility
// mymodeformability/MyModeFormAbility.ets

onAddForm(want: Want) {
  // Called to return a FormBindingData object.
  if (!want || !want.parameters) {
    console.error(`FormAbility onAddForm want or want.parameters is undefined`);
    return formBindingData.createFormBindingData('');
  }

  let formId: string = want.parameters[formInfo.FormParam.IDENTITY_KEY] as string;

  // store form id
  WidgetIdPrefs.addFormID(this.context, formId);
  console.log('set form id', formId)

  const data: ModeData = { mode: ModePrefs.getMode(this.context) };

  // send initial data to the widget
  return formBindingData.createFormBindingData(data);
}

onRemoveForm(formId: string) {
  // Called to notify the form provider that a specified form has been destroyed.
  // remove form id
  WidgetIdPrefs.removeFormID(this.context, formId);
}
Enter fullscreen mode Exit fullscreen mode

Step 6: Modify Entrybility

  • to catch the call action.
// entryability/EntryAbility.ets

onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {
  this.context.getApplicationContext().setColorMode(ConfigurationConstant.ColorMode.COLOR_MODE_NOT_SET);
  hilog.info(DOMAIN, 'testTag', '%{public}s', 'Ability onCreate');

  // postCardAction 'call' is caught here
  this.callee.on('changeMode', (data: rpc.MessageSequence) => {
    // Obtain all parameters passed in the call event.
    const dataObj: ModeData = JSON.parse(data.readString())
    console.log('ModeData', JSON.stringify(dataObj), dataObj.mode);

    AppStorage.set('mode', dataObj.mode)
    ModePrefs.setMode(this.context, dataObj.mode);
    vibrator.startVibration({ type: 'time', duration: 200 }, { usage: 'physicalFeedback' })
    return new ModeParcelable(dataObj.mode);
  });
}
Enter fullscreen mode Exit fullscreen mode
  • to store mode in PersistentStorage (to update UI directly)
onWindowStageCreate(windowStage: window.WindowStage): void {
  // Main window is created, set main page for this ability
  hilog.info(DOMAIN, 'testTag', '%{public}s', 'Ability onWindowStageCreate');

  windowStage.loadContent('pages/Index', (err) => {
    if (err.code) {
      hilog.error(DOMAIN, 'testTag', 'Failed to load the content. Cause: %{public}s', JSON.stringify(err));
      return;
    }
    hilog.info(DOMAIN, 'testTag', 'Succeeded in loading the content.');

    const mode = ModePrefs.getMode(this.context)
    PersistentStorage.persistProp('mode', mode)
    AppStorage.set('mode', mode)
  });
}
Enter fullscreen mode Exit fullscreen mode

Step 7: Modify Index.ets to show current mode.

  • Index
import { Mode } from '../model/Mode';
import { ModeData } from '../model/ModeParcelable';
import ModePrefs from '../util/ModePrefs';
import WidgetIdPrefs from '../util/WidgetIdPrefs';
import { formBindingData, formProvider } from '@kit.FormKit';


@Entry
@Component
struct Index {
  @StorageLink('mode') mode: Mode = ModePrefs.getMode(this.getUIContext().getHostContext() as Context)

  build() {
    Row({ space: 8 }) {
      Button('😭')
        .fontSize(18)
        .backgroundColor(this.mode === Mode.sad ? Color.Red : Color.Grey)
        .type(ButtonType.Circle)
        .layoutWeight(1)
        .onClick(() => {
          this.changeMode(Mode.sad)
        })
      Button('😑')
        .fontSize(18)
        .backgroundColor(this.mode === Mode.neutral ? Color.Yellow : Color.Grey)
        .type(ButtonType.Circle)
        .layoutWeight(1)
        .onClick(() => {
          this.changeMode(Mode.neutral)
        })
      Button('😊')
        .fontSize(18)
        .backgroundColor(this.mode === Mode.happy ? Color.Green : Color.Grey)
        .type(ButtonType.Circle)
        .layoutWeight(1)
        .onClick(() => {
          this.changeMode(Mode.happy)
        })
    }
    .padding(8)
    .height('100%')
    .backgroundColor($r('sys.color.comp_background_primary'))
  }

  changeMode(newMode: Mode) {
    if (this.mode !== newMode) {
      this.mode = newMode
      ModePrefs.setMode(this.getUIContext().getHostContext() as Context, newMode)
      const ids = WidgetIdPrefs.getFormIDs(this.getUIContext().getHostContext() as Context)
      // update widgets
      ids.forEach((id: string) => {
        const data: ModeData = { 'mode': newMode }
        let formInfo: formBindingData.FormBindingData = formBindingData.createFormBindingData(data);
        formProvider.updateForm(id, formInfo)
      })
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Step 8: Add a widget and see two-way synchronization.

Written by Mehmet Karaaslan

Top comments (0)