• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1# Widget Host Development (for System Applications Only)
2
3## Widget Overview
4
5A widget is a set of UI components that display important information or operations specific to an application. It provides users with direct access to a desired application service, without the need to open the application first.
6
7A widget usually appears as a part of the UI of another application (which currently can only be a system application) and provides basic interactive features such as opening a UI page or sending a message. The widget host is responsible for displaying the service widget.
8
9- Before you get started, it would be helpful if you have a basic understanding of the following concepts:
10
11  - Widget provider: an atomic service that controls the widget content to display, how widget components are laid out, and how they interact with users.
12
13  - Widget host: an application that displays the widget content and controls the widget location.
14
15  - Widget Manager: a resident agent that provides widget management features such as periodic widget updates.
16
17   ![formHostMoudle](./figures/widget-host-development-guide-1.png)
18
19## When to Use
20
21Carry out the following operations to develop the widget host based on the stage model:
22
23- Use **FormComponent**.
24- Use the APIs provided by the **formHost** module to delete or update widgets.
25
26## Using FormComponent
27
28**FormComponent** is a component used to display widgets. For details, see [FormComponent](../reference/apis-arkui/arkui-ts/ts-basic-components-formcomponent-sys.md).
29
30> **NOTE**
31>
32> - This component is supported since API version 7. Updates will be marked with a superscript to indicate their earliest API version.
33>
34> - This component functions as the widget host.
35>
36> - To use this component, you must have the system signature.
37>
38> - The APIs provided by this component are system APIs.
39
40When a widget is added through **FormComponent**, the [onAddForm](../reference/apis-form-kit/js-apis-app-form-formExtensionAbility.md#formextensionabilityonaddform) API in **FormExtensionAbility** of the widget provider is called.
41
42### Temporary and Normal Widgets
43
44The **temporary** field in **FormComponent** specifies whether a widget is a temporary or normal widget. The value **true** indicates a temporary widget, and **false** indicates a normal widget.
45
46- Normal widget: a widget persistently used by the widget host, for example, a widget added to the home screen.
47
48- Temporary widget: a widget temporarily used by the widget host, for example, the widget displayed when you swipe up on a widget application.
49
50Data of a temporary widget will be deleted on the Widget Manager if the widget framework is killed and restarted. The widget provider, however, is not notified of the deletion and still keeps the data. Therefore, the widget provider needs to clear the data of temporary widgets proactively if the data has been kept for a long period of time. If the widget host has converted a temporary widget into a normal one, the widget provider should change the widget data from temporary storage to persistent storage. Otherwise, the widget data may be deleted by mistake.
51
52## Using formHost APIs
53
54The **formHost** module provides a series of APIs for the widget host to update and delete widgets. For details, see the [API reference](../reference/apis-form-kit/js-apis-app-form-formHost-sys.md).
55
56## Example
57
58```ts
59//Index.ets
60import { HashMap, HashSet } from '@kit.ArkTS';
61import { formHost, formInfo, formObserver } from '@kit.FormKit';
62import { bundleMonitor } from '@kit.AbilityKit';
63import { BusinessError } from '@kit.BasicServicesKit';
64
65@Entry
66@Component
67struct formHostSample {
68  // Enumerated values of the widget size.
69  static FORM_DIMENSIONS_MAP = [
70    '1*2',
71    '2*2',
72    '2*4',
73    '4*4',
74    '1*1',
75    '6*4',
76  ]
77
78  // Simulate the widget sizes.
79  static FORM_SIZE = [
80    [120, 60],    // 1*2
81    [120, 120],   // 2*2
82    [240, 120],   // 2*4
83    [240, 240],   // 4*4
84    [60, 60],     // 1*1
85    [240, 360],   // 6*4
86  ]
87
88  @State message: Resource | string = $r('app.string.Host');
89  formCardHashMap: HashMap<string, formInfo.FormInfo> = new HashMap();
90  @State showFormPicker: boolean = false;
91  @State operation: Resource | string = $r('app.string.formOperation');
92  @State index: number = 2;
93  @State space: number = 8;
94  @State arrowPosition: ArrowPosition = ArrowPosition.END;
95  formIds: HashSet<string> = new HashSet();
96  currentFormKey: string = '';
97  focusFormInfo: formInfo.FormInfo = {
98    bundleName: '',
99    moduleName: '',
100    abilityName: '',
101    name: '',
102    displayName: '',
103    displayNameId: 0,
104    description: '',
105    descriptionId: 0,
106    type: formInfo.FormType.eTS,
107    jsComponentName: '',
108    isDefault: false,
109    updateEnabled: false,
110    formVisibleNotify: true,
111    scheduledUpdateTime: '',
112    formConfigAbility: '',
113    updateDuration: 0,
114    defaultDimension: 6,
115    supportDimensions: [],
116    supportedShapes: [],
117    customizeData: {},
118    isDynamic: false,
119    transparencyEnabled: false
120  }
121  formInfoRecord: TextCascadePickerRangeContent[] = [];
122  pickerBtnMsg: Resource | string = $r('app.string.formType');
123  @State showForm: boolean = true;
124  @State selectFormId: string = '0';
125  @State pickDialogIndex: number = 0;
126
127  aboutToAppear(): void {
128    try {
129      // Check whether the system is ready.
130      formHost.isSystemReady().then(() => {
131        console.info('formHost isSystemReady success');
132
133        // Subscribe to events indicating that a widget becomes invisible and events indicating that a widget becomes visible.
134        let notifyInvisibleCallback = (data: formInfo.RunningFormInfo[]) => {
135          console.info(`form change invisibility, data: ${JSON.stringify(data)}`);
136        }
137        let notifyVisibleCallback = (data: formInfo.RunningFormInfo[]) => {
138          console.info(`form change visibility, data: ${JSON.stringify(data)}`);
139        }
140        formObserver.on('notifyInvisible', notifyInvisibleCallback);
141        formObserver.on('notifyVisible', notifyVisibleCallback);
142
143        // Subscribe to bundle installation events.
144        try {
145          bundleMonitor.on('add', (bundleChangeInfo) => {
146            console.info(`bundleName : ${bundleChangeInfo.bundleName} userId : ${bundleChangeInfo.userId}`);
147            this.getAllBundleFormsInfo();
148          })
149        } catch (errData) {
150          let message = (errData as BusinessError).message;
151          let errCode = (errData as BusinessError).code;
152          console.error(`errData is errCode:${errCode}  message:${message}`);
153        }
154        // Subscribe to bundle update events.
155        try {
156          bundleMonitor.on('update', (bundleChangeInfo) => {
157            console.info(`bundleName : ${bundleChangeInfo.bundleName} userId : ${bundleChangeInfo.userId}`);
158            this.getAllBundleFormsInfo();
159          })
160        } catch (errData) {
161          let message = (errData as BusinessError).message;
162          let errCode = (errData as BusinessError).code;
163          console.error(`errData is errCode:${errCode}  message:${message}`);
164        }
165        // Subscribe to bundle uninstall events.
166        try {
167          bundleMonitor.on('remove', (bundleChangeInfo) => {
168            console.info(`bundleName : ${bundleChangeInfo.bundleName} userId : ${bundleChangeInfo.userId}`);
169            this.getAllBundleFormsInfo();
170          })
171        } catch (errData) {
172          let message = (errData as BusinessError).message;
173          let errCode = (errData as BusinessError).code;
174          console.error(`errData is errCode:${errCode}  message:${message}`);
175        }
176      }).catch((error: BusinessError) => {
177        console.error(`error, code: ${error.code}, message: ${error.message}`);
178      });
179    }
180    catch (error) {
181      console.error(`catch error, code: ${(error as BusinessError).code}, message: ${(error as BusinessError).message}`);
182    }
183  }
184
185  aboutToDisappear(): void {
186    // Delete all widgets.
187    this.formIds.forEach((id) => {
188      console.info('delete all form')
189      formHost.deleteForm(id);
190    });
191    // Unsubscribe from bundle installation events.
192    try {
193      bundleMonitor.off('add');
194    } catch (errData) {
195      let message = (errData as BusinessError).message;
196      let errCode = (errData as BusinessError).code;
197      console.error(`errData is errCode:${errCode}  message:${message}`);
198    }
199    // Unsubscribe from bundle update events.
200    try {
201      bundleMonitor.off('update');
202    } catch (errData) {
203      let message = (errData as BusinessError).message;
204      let errCode = (errData as BusinessError).code;
205      console.error(`errData is errCode:${errCode}  message:${message}`);
206    }
207    // Unsubscribe from bundle uninstall events.
208    try {
209      bundleMonitor.off('remove');
210    } catch (errData) {
211      let message = (errData as BusinessError).message;
212      let errCode = (errData as BusinessError).code;
213      console.error(`errData is errCode:${errCode}  message:${message}`);
214    }
215    // Unsubscribe from events indicating that a widget becomes invisible and events indicating that a widget becomes visible.
216    formObserver.off('notifyInvisible');
217    formObserver.off('notifyVisible');
218  }
219
220  // Save the information of all widgets to formHapRecordMap.
221  getAllBundleFormsInfo() {
222    this.formCardHashMap.clear();
223    this.showFormPicker = false;
224    let formHapRecordMap: HashMap<string, formInfo.FormInfo[]> = new HashMap();
225    this.formInfoRecord = [];
226    formHost.getAllFormsInfo().then((formList: Array<formInfo.FormInfo>) => {
227      console.info('getALlFormsInfo size:' + formList.length)
228      for (let formItemInfo of formList) {
229        let formBundleName = formItemInfo.bundleName;
230        if (formHapRecordMap.hasKey(formBundleName)) {
231          formHapRecordMap.get(formBundleName).push(formItemInfo)
232        } else {
233          let formInfoList: formInfo.FormInfo[] = [formItemInfo];
234          formHapRecordMap.set(formBundleName, formInfoList);
235        }
236      }
237      for (let formBundle of formHapRecordMap.keys()) {
238        let bundleFormInfo: TextCascadePickerRangeContent = {
239          text: formBundle,
240          children: []
241        }
242        let bundleFormList: formInfo.FormInfo[] = formHapRecordMap.get(formBundle);
243        bundleFormList.forEach((formItemInfo) => {
244          let dimensionName = formHostSample.FORM_DIMENSIONS_MAP[formItemInfo.defaultDimension - 1];
245          bundleFormInfo.children?.push({ text: formItemInfo.name + '#' + dimensionName });
246          this.formCardHashMap.set(formBundle + "#" + formItemInfo.name + '#' + dimensionName, formItemInfo);
247        })
248        this.formInfoRecord.push(bundleFormInfo);
249      }
250      this.formCardHashMap.forEach((formItem: formInfo.FormInfo) => {
251        console.info(`formCardHashmap: ${JSON.stringify(formItem)}`);
252      })
253      this.showFormPicker = true;
254    })
255  }
256
257  build() {
258    Column() {
259      Text(this.message)
260        .fontSize(30)
261        .fontWeight(FontWeight.Bold)
262
263      Divider().vertical(false).color(Color.Black).lineCap(LineCapStyle.Butt).margin({ top: 10, bottom: 10 })
264
265      Row() {
266        // Click to query information about all widgets.
267        Button($r('app.string.inquiryForm'))
268          .onClick(() => {
269            this.getAllBundleFormsInfo();
270          })
271
272        // After the user clicks a button, a selection page is displayed. After the user clicks OK, the selected widget of the default size is added.
273        Button($r('app.string.selectAddForm'))
274          .enabled(this.showFormPicker)
275          .onClick(() => {
276            console.info("showTextPickerDialog")
277            this.getUIContext().showTextPickerDialog({
278              range: this.formInfoRecord,
279              selected: this.pickDialogIndex,
280              canLoop: false,
281              disappearTextStyle: { color: Color.Red, font: { size: 10, weight: FontWeight.Lighter } },
282              textStyle: { color: Color.Black, font: { size: 12, weight: FontWeight.Normal } },
283              selectedTextStyle: { color: Color.Blue, font: { size: 12, weight: FontWeight.Bolder } },
284              onAccept: (result: TextPickerResult) => {
285                this.currentFormKey = result.value[0] + "#" + result.value[1];
286                this.pickDialogIndex = result.index[0]
287                console.info(`TextPickerDialog onAccept: ${this.currentFormKey}, ${this.pickDialogIndex}`);
288                if (!this.formCardHashMap.hasKey(this.currentFormKey)) {
289                  console.error(`invalid formItemInfo by form key`)
290                  return;
291                }
292                this.showForm = true;
293                this.focusFormInfo = this.formCardHashMap.get(this.currentFormKey);
294              },
295              onCancel: () => {
296                console.info("TextPickerDialog : onCancel()")
297              },
298              onChange: (result: TextPickerResult) => {
299                this.pickerBtnMsg = result.value[0] + '#' + result.value[1];
300                console.info("TextPickerDialog:onChange:" + this.pickerBtnMsg)
301              }
302            })
303          })
304          .margin({ left: 10 })
305      }
306      .margin({ left: 10 })
307
308      Divider().vertical(false).color(Color.Black).lineCap(LineCapStyle.Butt).margin({ top: 10, bottom: 10 })
309
310      if(this.showForm){
311        Text(this.pickerBtnMsg)
312          .margin({ top: 10, bottom: 10 })
313      }
314
315      if (this.showForm) {
316        Text('formId: ' + this.selectFormId)
317          .margin({ top: 10, bottom: 10 })
318
319        // FormComponent
320        FormComponent({
321          id: Number.parseInt(this.selectFormId),
322          name: this.focusFormInfo.name,
323          bundle: this.focusFormInfo.bundleName,
324          ability: this.focusFormInfo.abilityName,
325          module: this.focusFormInfo.moduleName,
326          dimension: this.focusFormInfo.defaultDimension,
327          temporary: false,
328        })
329          .size({
330            width: formHostSample.FORM_SIZE[this.focusFormInfo.defaultDimension - 1][0],
331            height: formHostSample.FORM_SIZE[this.focusFormInfo.defaultDimension - 1][1],
332          })
333          .borderColor(Color.Black)
334          .borderRadius(10)
335          .borderWidth(1)
336          .onAcquired((form: FormCallbackInfo) => {
337            console.info(`onAcquired: ${JSON.stringify(form)}`)
338            this.selectFormId = form.id.toString();
339            this.formIds.add(this.selectFormId);
340          })
341          .onRouter(() => {
342            console.info(`onRouter`)
343          })
344          .onError((error) => {
345            console.error(`onError: ${JSON.stringify(error)}`)
346            this.showForm = false;
347          })
348          .onUninstall((info: FormCallbackInfo) => {
349            this.showForm = false;
350            console.info(`onUninstall: ${JSON.stringify(info)}`)
351            this.formIds.remove(this.selectFormId);
352          })
353
354        // A select list that displays some formHost APIs
355        Row() {
356          Select([{ value: $r('app.string.deleteForm') },
357            { value: $r('app.string.updateForm') },
358            { value: $r('app.string.visibleForms') },
359            { value: $r('app.string.invisibleForms') },
360            { value: $r('app.string.enableFormsUpdate') },
361            { value: $r('app.string.disableFormsUpdate') },
362          ])
363            .selected(this.index)
364            .value(this.operation)
365            .font({ size: 16, weight: 500 })
366            .fontColor('#182431')
367            .selectedOptionFont({ size: 16, weight: 400 })
368            .optionFont({ size: 16, weight: 400 })
369            .space(this.space)
370            .arrowPosition(this.arrowPosition)
371            .menuAlign(MenuAlignType.START, { dx: 0, dy: 0 })
372            .optionWidth(200)
373            .optionHeight(300)
374            .onSelect((index: number, text?: string | Resource) => {
375              console.info('Select:' + index)
376              this.index = index;
377              if (text) {
378                this.operation = text;
379              }
380            })
381
382          // Operate the widget based on what selected in the select list.
383          Button($r('app.string.execute'), {
384            type: ButtonType.Capsule
385          })
386            .fontSize(16)
387            .onClick(() => {
388              switch (this.index) {
389                case 0:
390                  try {
391                    formHost.deleteForm(this.selectFormId, (error: BusinessError) => {
392                      if (error) {
393                        console.error(`deleteForm error, code: ${error.code}, message: ${error.message}`);
394                      } else {
395                        console.info('formHost deleteForm success');
396                      }
397                    });
398                  } catch (error) {
399                    console.error(`deleteForm catch error, code: ${(error as BusinessError).code}, message: ${(error as BusinessError).message}`);
400                  }
401                  this.showForm = false;
402                  this.selectFormId = '';
403                  break;
404                case 1:
405                  try {
406                    formHost.requestForm(this.selectFormId, (error: BusinessError) => {
407                      if (error) {
408                        console.error(`requestForm error, code: ${error.code}, message: ${error.message}`);
409                      }
410                    });
411                  } catch (error) {
412                    console.error(`requestForm catch error, code: ${(error as BusinessError).code}, message: ${(error as BusinessError).message}`);
413                  }
414                  break;
415                case 2:
416                  try {
417                    formHost.notifyVisibleForms([this.selectFormId], (error: BusinessError) => {
418                      if (error) {
419                        console.error(`notifyVisibleForms error, code: ${error.code}, message: ${error.message}`);
420                      } else {
421                        console.info('notifyVisibleForms success');
422                      }
423                    });
424                  } catch (error) {
425                    console.error(`notifyVisibleForms catch error, code: ${(error as BusinessError).code}, message: ${(error as BusinessError).message}`);
426                  }
427                  break;
428                case 3:
429                  try {
430                    formHost.notifyInvisibleForms([this.selectFormId], (error: BusinessError) => {
431                      if (error) {
432                        console.error(`notifyInvisibleForms error, code: ${error.code}, message: ${error.message}`);
433                      } else {
434                        console.info('notifyInvisibleForms success');
435                      }
436                    });
437                  } catch (error) {
438                    console.error(`notifyInvisibleForms catch error, code: ${(error as BusinessError).code}, message: ${(error as BusinessError).message}`);
439                  }
440                  break;
441                case 4:
442                  try {
443                    formHost.enableFormsUpdate([this.selectFormId], (error: BusinessError) => {
444                      if (error) {
445                        console.error(`enableFormsUpdate error, code: ${error.code}, message: ${error.message}`);
446                      }
447                    });
448                  } catch (error) {
449                    console.error(`enableFormsUpdate catch error, code: ${(error as BusinessError).code}, message: ${(error as BusinessError).message}`);
450                  }
451                  break;
452                case 5:
453                  try {
454                    formHost.disableFormsUpdate([this.selectFormId], (error: BusinessError) => {
455                      if (error) {
456                        console.error(`disableFormsUpdate error, code: ${error.code}, message: ${error.message}`);
457                      } else {
458                        console.info('disableFormsUpdate success');
459                      }
460                    });
461                  } catch (error) {
462                    console.error(`disableFormsUpdate catch error, code: ${(error as BusinessError).code}, message: ${(error as BusinessError).message}`);
463                  }
464                  break;
465              }
466            })
467        }
468        .margin({
469          top: 20,
470          bottom: 10
471        })
472      }
473    }
474  }
475}
476```
477
478![screenshot](./figures/widget-host-development-guide-2.jpeg)
479