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#onaddform) 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    '2*1',
75    '1*1',
76    '6*4',
77  ]
78
79  // Simulate the widget sizes.
80  static FORM_SIZE = [
81    [120, 60],    // 1*2
82    [120, 120],   // 2*2
83    [240, 120],   // 2*4
84    [240, 240],   // 4*4
85    [60, 120],    // 2*1
86    [60, 60],     // 1*1
87    [240, 360],   // 6*4
88  ]
89
90  @State message: Resource | string = $r('app.string.Host');
91  formCardHashMap: HashMap<string, formInfo.FormInfo> = new HashMap();
92  @State showFormPicker: boolean = false;
93  @State operation: Resource | string = $r('app.string.formOperation');
94  @State index: number = 2;
95  @State space: number = 8;
96  @State arrowPosition: ArrowPosition = ArrowPosition.END;
97  formIds: HashSet<string> = new HashSet();
98  currentFormKey: string = '';
99  focusFormInfo: formInfo.FormInfo = {
100    bundleName: '',
101    moduleName: '',
102    abilityName: '',
103    name: '',
104    displayName: '',
105    displayNameId: 0,
106    description: '',
107    descriptionId: 0,
108    type: formInfo.FormType.eTS,
109    jsComponentName: '',
110    colorMode: formInfo.ColorMode.MODE_AUTO,
111    isDefault: false,
112    updateEnabled: false,
113    formVisibleNotify: true,
114    scheduledUpdateTime: '',
115    formConfigAbility: '',
116    updateDuration: 0,
117    defaultDimension: 6,
118    supportDimensions: [],
119    supportedShapes: [],
120    customizeData: {},
121    isDynamic: false,
122    transparencyEnabled: false
123  }
124  formInfoRecord: TextCascadePickerRangeContent[] = [];
125  pickerBtnMsg: Resource | string = $r('app.string.formType');
126  @State showForm: boolean = true;
127  @State selectFormId: string = '0';
128  @State pickDialogIndex: number = 0;
129
130  aboutToAppear(): void {
131    try {
132      // Check whether the system is ready.
133      formHost.isSystemReady().then(() => {
134        console.log('formHost isSystemReady success');
135
136        // Subscribe to events indicating that a widget becomes invisible and events indicating that a widget becomes visible.
137        let notifyInvisibleCallback = (data: formInfo.RunningFormInfo[]) => {
138          console.log(`form change invisibility, data: ${JSON.stringify(data)}`);
139        }
140        let notifyVisibleCallback = (data: formInfo.RunningFormInfo[]) => {
141          console.log(`form change visibility, data: ${JSON.stringify(data)}`);
142        }
143        formObserver.on('notifyInvisible', notifyInvisibleCallback);
144        formObserver.on('notifyVisible', notifyVisibleCallback);
145
146        // Subscribe to bundle installation events.
147        try {
148          bundleMonitor.on('add', (bundleChangeInfo) => {
149            console.info(`bundleName : ${bundleChangeInfo.bundleName} userId : ${bundleChangeInfo.userId}`);
150            this.getAllBundleFormsInfo();
151          })
152        } catch (errData) {
153          let message = (errData as BusinessError).message;
154          let errCode = (errData as BusinessError).code;
155          console.log(`errData is errCode:${errCode}  message:${message}`);
156        }
157        // Subscribe to bundle update events.
158        try {
159          bundleMonitor.on('update', (bundleChangeInfo) => {
160            console.info(`bundleName : ${bundleChangeInfo.bundleName} userId : ${bundleChangeInfo.userId}`);
161            this.getAllBundleFormsInfo();
162          })
163        } catch (errData) {
164          let message = (errData as BusinessError).message;
165          let errCode = (errData as BusinessError).code;
166          console.log(`errData is errCode:${errCode}  message:${message}`);
167        }
168        // Subscribe to bundle uninstall events.
169        try {
170          bundleMonitor.on('remove', (bundleChangeInfo) => {
171            console.info(`bundleName : ${bundleChangeInfo.bundleName} userId : ${bundleChangeInfo.userId}`);
172            this.getAllBundleFormsInfo();
173          })
174        } catch (errData) {
175          let message = (errData as BusinessError).message;
176          let errCode = (errData as BusinessError).code;
177          console.log(`errData is errCode:${errCode}  message:${message}`);
178        }
179      }).catch((error: BusinessError) => {
180        console.error(`error, code: ${error.code}, message: ${error.message}`);
181      });
182    }
183    catch (error) {
184      console.error(`catch error, code: ${(error as BusinessError).code}, message: ${(error as BusinessError).message}`);
185    }
186  }
187
188  aboutToDisappear(): void {
189    // Delete all widgets.
190    this.formIds.forEach((id) => {
191      console.log('delete all form')
192      formHost.deleteForm(id);
193    });
194    // Unsubscribe from bundle installation events.
195    try {
196      bundleMonitor.off('add');
197    } catch (errData) {
198      let message = (errData as BusinessError).message;
199      let errCode = (errData as BusinessError).code;
200      console.log(`errData is errCode:${errCode}  message:${message}`);
201    }
202    // Unsubscribe from bundle update events.
203    try {
204      bundleMonitor.off('update');
205    } catch (errData) {
206      let message = (errData as BusinessError).message;
207      let errCode = (errData as BusinessError).code;
208      console.log(`errData is errCode:${errCode}  message:${message}`);
209    }
210    // Unsubscribe from bundle uninstall events.
211    try {
212      bundleMonitor.off('remove');
213    } catch (errData) {
214      let message = (errData as BusinessError).message;
215      let errCode = (errData as BusinessError).code;
216      console.log(`errData is errCode:${errCode}  message:${message}`);
217    }
218    // Unsubscribe from events indicating that a widget becomes invisible and events indicating that a widget becomes visible.
219    formObserver.off('notifyInvisible');
220    formObserver.off('notifyVisible');
221  }
222
223  // Save the information of all widgets to formHapRecordMap.
224  getAllBundleFormsInfo() {
225    this.formCardHashMap.clear();
226    this.showFormPicker = false;
227    let formHapRecordMap: HashMap<string, formInfo.FormInfo[]> = new HashMap();
228    this.formInfoRecord = [];
229    formHost.getAllFormsInfo().then((formList: Array<formInfo.FormInfo>) => {
230      console.log('getALlFormsInfo size:' + formList.length)
231      for (let formItemInfo of formList) {
232        let formBundleName = formItemInfo.bundleName;
233        if (formHapRecordMap.hasKey(formBundleName)) {
234          formHapRecordMap.get(formBundleName).push(formItemInfo)
235        } else {
236          let formInfoList: formInfo.FormInfo[] = [formItemInfo];
237          formHapRecordMap.set(formBundleName, formInfoList);
238        }
239      }
240      for (let formBundle of formHapRecordMap.keys()) {
241        let bundleFormInfo: TextCascadePickerRangeContent = {
242          text: formBundle,
243          children: []
244        }
245        let bundleFormList: formInfo.FormInfo[] = formHapRecordMap.get(formBundle);
246        bundleFormList.forEach((formItemInfo) => {
247          let dimensionName = formHostSample.FORM_DIMENSIONS_MAP[formItemInfo.defaultDimension - 1];
248          bundleFormInfo.children?.push({ text: formItemInfo.name + '#' + dimensionName });
249          this.formCardHashMap.set(formBundle + "#" + formItemInfo.name + '#' + dimensionName, formItemInfo);
250        })
251        this.formInfoRecord.push(bundleFormInfo);
252      }
253      this.formCardHashMap.forEach((formItem: formInfo.FormInfo) => {
254        console.info(`formCardHashmap: ${JSON.stringify(formItem)}`);
255      })
256      this.showFormPicker = true;
257    })
258  }
259
260  build() {
261    Column() {
262      Text(this.message)
263        .fontSize(30)
264        .fontWeight(FontWeight.Bold)
265
266      Divider().vertical(false).color(Color.Black).lineCap(LineCapStyle.Butt).margin({ top: 10, bottom: 10 })
267
268      Row() {
269        // Click to query information about all widgets.
270        Button($r('app.string.inquiryForm'))
271          .onClick(() => {
272            this.getAllBundleFormsInfo();
273          })
274
275        // 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.
276        Button($r('app.string.selectAddForm'))
277          .enabled(this.showFormPicker)
278          .onClick(() => {
279            console.info("TextPickerDialog: show()")
280            TextPickerDialog.show({
281              range: this.formInfoRecord,
282              selected: this.pickDialogIndex,
283              canLoop: false,
284              disappearTextStyle: { color: Color.Red, font: { size: 10, weight: FontWeight.Lighter } },
285              textStyle: { color: Color.Black, font: { size: 12, weight: FontWeight.Normal } },
286              selectedTextStyle: { color: Color.Blue, font: { size: 12, weight: FontWeight.Bolder } },
287              onAccept: (result: TextPickerResult) => {
288                this.currentFormKey = result.value[0] + "#" + result.value[1];
289                this.pickDialogIndex = result.index[0]
290                console.info(`TextPickerDialog onAccept: ${this.currentFormKey}, ${this.pickDialogIndex}`);
291                if (!this.formCardHashMap.hasKey(this.currentFormKey)) {
292                  console.error(`invalid formItemInfo by form key`)
293                  return;
294                }
295                this.showForm = true;
296                this.focusFormInfo = this.formCardHashMap.get(this.currentFormKey);
297              },
298              onCancel: () => {
299                console.info("TextPickerDialog : onCancel()")
300              },
301              onChange: (result: TextPickerResult) => {
302                this.pickerBtnMsg = result.value[0] + '#' + result.value[1];
303                console.info("TextPickerDialog:onChange:" + this.pickerBtnMsg)
304              }
305            })
306          })
307          .margin({ left: 10 })
308      }
309      .margin({ left: 10 })
310
311      Divider().vertical(false).color(Color.Black).lineCap(LineCapStyle.Butt).margin({ top: 10, bottom: 10 })
312
313      if(this.showForm){
314        Text(this.pickerBtnMsg)
315          .margin({ top: 10, bottom: 10 })
316      }
317
318      if (this.showForm) {
319        Text('formId: ' + this.selectFormId)
320          .margin({ top: 10, bottom: 10 })
321
322        // FormComponent
323        FormComponent({
324          id: Number.parseInt(this.selectFormId),
325          name: this.focusFormInfo.name,
326          bundle: this.focusFormInfo.bundleName,
327          ability: this.focusFormInfo.abilityName,
328          module: this.focusFormInfo.moduleName,
329          dimension: this.focusFormInfo.defaultDimension,
330          temporary: false,
331        })
332          .size({
333            width: formHostSample.FORM_SIZE[this.focusFormInfo.defaultDimension - 1][0],
334            height: formHostSample.FORM_SIZE[this.focusFormInfo.defaultDimension - 1][1],
335          })
336          .borderColor(Color.Black)
337          .borderRadius(10)
338          .borderWidth(1)
339          .onAcquired((form: FormCallbackInfo) => {
340            console.log(`onAcquired: ${JSON.stringify(form)}`)
341            this.selectFormId = form.id.toString();
342            this.formIds.add(this.selectFormId);
343          })
344          .onRouter(() => {
345            console.log(`onRouter`)
346          })
347          .onError((error) => {
348            console.error(`onError: ${JSON.stringify(error)}`)
349            this.showForm = false;
350          })
351          .onUninstall((info: FormCallbackInfo) => {
352            this.showForm = false;
353            console.error(`onUninstall: ${JSON.stringify(info)}`)
354            this.formIds.remove(this.selectFormId);
355          })
356
357        // A select list that displays some formHost APIs
358        Row() {
359          Select([{ value: $r('app.string.deleteForm') },
360            { value: $r('app.string.updateForm') },
361            { value: $r('app.string.visibleForms') },
362            { value: $r('app.string.invisibleForms') },
363            { value: $r('app.string.enableFormsUpdate') },
364            { value: $r('app.string.disableFormsUpdate') },
365          ])
366            .selected(this.index)
367            .value(this.operation)
368            .font({ size: 16, weight: 500 })
369            .fontColor('#182431')
370            .selectedOptionFont({ size: 16, weight: 400 })
371            .optionFont({ size: 16, weight: 400 })
372            .space(this.space)
373            .arrowPosition(this.arrowPosition)
374            .menuAlign(MenuAlignType.START, { dx: 0, dy: 0 })
375            .optionWidth(200)
376            .optionHeight(300)
377            .onSelect((index: number, text?: string | Resource) => {
378              console.info('Select:' + index)
379              this.index = index;
380              if (text) {
381                this.operation = text;
382              }
383            })
384
385          // Operate the widget based on what selected in the select list.
386          Button($r('app.string.execute'), {
387            type: ButtonType.Capsule
388          })
389            .fontSize(16)
390            .onClick(() => {
391              switch (this.index) {
392                case 0:
393                  try {
394                    formHost.deleteForm(this.selectFormId, (error: BusinessError) => {
395                      if (error) {
396                        console.error(`deleteForm error, code: ${error.code}, message: ${error.message}`);
397                      } else {
398                        console.log('formHost deleteForm success');
399                      }
400                    });
401                  } catch (error) {
402                    console.error(`deleteForm catch error, code: ${(error as BusinessError).code}, message: ${(error as BusinessError).message}`);
403                  }
404                  this.showForm = false;
405                  this.selectFormId = '';
406                  break;
407                case 1:
408                  try {
409                    formHost.requestForm(this.selectFormId, (error: BusinessError) => {
410                      if (error) {
411                        console.error(`requestForm error, code: ${error.code}, message: ${error.message}`);
412                      }
413                    });
414                  } catch (error) {
415                    console.error(`requestForm catch error, code: ${(error as BusinessError).code}, message: ${(error as BusinessError).message}`);
416                  }
417                  break;
418                case 2:
419                  try {
420                    formHost.notifyVisibleForms([this.selectFormId], (error: BusinessError) => {
421                      if (error) {
422                        console.error(`notifyVisibleForms error, code: ${error.code}, message: ${error.message}`);
423                      } else {
424                        console.info('notifyVisibleForms success');
425                      }
426                    });
427                  } catch (error) {
428                    console.error(`notifyVisibleForms catch error, code: ${(error as BusinessError).code}, message: ${(error as BusinessError).message}`);
429                  }
430                  break;
431                case 3:
432                  try {
433                    formHost.notifyInvisibleForms([this.selectFormId], (error: BusinessError) => {
434                      if (error) {
435                        console.error(`notifyInvisibleForms error, code: ${error.code}, message: ${error.message}`);
436                      } else {
437                        console.info('notifyInvisibleForms success');
438                      }
439                    });
440                  } catch (error) {
441                    console.error(`notifyInvisibleForms catch error, code: ${(error as BusinessError).code}, message: ${(error as BusinessError).message}`);
442                  }
443                  break;
444                case 4:
445                  try {
446                    formHost.enableFormsUpdate([this.selectFormId], (error: BusinessError) => {
447                      if (error) {
448                        console.error(`enableFormsUpdate error, code: ${error.code}, message: ${error.message}`);
449                      }
450                    });
451                  } catch (error) {
452                    console.error(`enableFormsUpdate catch error, code: ${(error as BusinessError).code}, message: ${(error as BusinessError).message}`);
453                  }
454                  break;
455                case 5:
456                  try {
457                    formHost.disableFormsUpdate([this.selectFormId], (error: BusinessError) => {
458                      if (error) {
459                        console.error(`disableFormsUpdate error, code: ${error.code}, message: ${error.message}`);
460                      } else {
461                        console.info('disableFormsUpdate success');
462                      }
463                    });
464                  } catch (error) {
465                    console.error(`disableFormsUpdate catch error, code: ${(error as BusinessError).code}, message: ${(error as BusinessError).message}`);
466                  }
467                  break;
468              }
469            })
470        }
471        .margin({
472          top: 20,
473          bottom: 10
474        })
475      }
476    }
477  }
478}
479```
480
481![screenshot](./figures/widget-host-development-guide-2.jpeg)
482