# Widget Host Development (for System Applications Only) ## Widget Overview A 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. A 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. - Before you get started, it would be helpful if you have a basic understanding of the following concepts: - Widget provider: an atomic service that controls the widget content to display, how widget components are laid out, and how they interact with users. - Widget host: an application that displays the widget content and controls the widget location. - Widget Manager: a resident agent that provides widget management features such as periodic widget updates. ![formHostMoudle](./figures/widget-host-development-guide-1.png) ## When to Use Carry out the following operations to develop the widget host based on the stage model: - Use **FormComponent**. - Use the APIs provided by the **formHost** module to delete or update widgets. ## Using FormComponent **FormComponent** is a component used to display widgets. For details, see [FormComponent](../reference/apis-arkui/arkui-ts/ts-basic-components-formcomponent-sys.md). > **NOTE** > > - This component is supported since API version 7. Updates will be marked with a superscript to indicate their earliest API version. > > - This component functions as the widget host. > > - To use this component, you must have the system signature. > > - The APIs provided by this component are system APIs. When 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. ### Temporary and Normal Widgets The **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. - Normal widget: a widget persistently used by the widget host, for example, a widget added to the home screen. - Temporary widget: a widget temporarily used by the widget host, for example, the widget displayed when you swipe up on a widget application. Data 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. ## Using formHost APIs The **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). ## Example ```ts //Index.ets import { HashMap, HashSet } from '@kit.ArkTS'; import { formHost, formInfo, formObserver } from '@kit.FormKit'; import { bundleMonitor } from '@kit.AbilityKit'; import { BusinessError } from '@kit.BasicServicesKit'; @Entry @Component struct formHostSample { // Enumerated values of the widget size. static FORM_DIMENSIONS_MAP = [ '1*2', '2*2', '2*4', '4*4', '2*1', '1*1', '6*4', ] // Simulate the widget sizes. static FORM_SIZE = [ [120, 60], // 1*2 [120, 120], // 2*2 [240, 120], // 2*4 [240, 240], // 4*4 [60, 120], // 2*1 [60, 60], // 1*1 [240, 360], // 6*4 ] @State message: Resource | string = $r('app.string.Host'); formCardHashMap: HashMap = new HashMap(); @State showFormPicker: boolean = false; @State operation: Resource | string = $r('app.string.formOperation'); @State index: number = 2; @State space: number = 8; @State arrowPosition: ArrowPosition = ArrowPosition.END; formIds: HashSet = new HashSet(); currentFormKey: string = ''; focusFormInfo: formInfo.FormInfo = { bundleName: '', moduleName: '', abilityName: '', name: '', displayName: '', displayNameId: 0, description: '', descriptionId: 0, type: formInfo.FormType.eTS, jsComponentName: '', colorMode: formInfo.ColorMode.MODE_AUTO, isDefault: false, updateEnabled: false, formVisibleNotify: true, scheduledUpdateTime: '', formConfigAbility: '', updateDuration: 0, defaultDimension: 6, supportDimensions: [], supportedShapes: [], customizeData: {}, isDynamic: false, transparencyEnabled: false } formInfoRecord: TextCascadePickerRangeContent[] = []; pickerBtnMsg: Resource | string = $r('app.string.formType'); @State showForm: boolean = true; @State selectFormId: string = '0'; @State pickDialogIndex: number = 0; aboutToAppear(): void { try { // Check whether the system is ready. formHost.isSystemReady().then(() => { console.log('formHost isSystemReady success'); // Subscribe to events indicating that a widget becomes invisible and events indicating that a widget becomes visible. let notifyInvisibleCallback = (data: formInfo.RunningFormInfo[]) => { console.log(`form change invisibility, data: ${JSON.stringify(data)}`); } let notifyVisibleCallback = (data: formInfo.RunningFormInfo[]) => { console.log(`form change visibility, data: ${JSON.stringify(data)}`); } formObserver.on('notifyInvisible', notifyInvisibleCallback); formObserver.on('notifyVisible', notifyVisibleCallback); // Subscribe to bundle installation events. try { bundleMonitor.on('add', (bundleChangeInfo) => { console.info(`bundleName : ${bundleChangeInfo.bundleName} userId : ${bundleChangeInfo.userId}`); this.getAllBundleFormsInfo(); }) } catch (errData) { let message = (errData as BusinessError).message; let errCode = (errData as BusinessError).code; console.log(`errData is errCode:${errCode} message:${message}`); } // Subscribe to bundle update events. try { bundleMonitor.on('update', (bundleChangeInfo) => { console.info(`bundleName : ${bundleChangeInfo.bundleName} userId : ${bundleChangeInfo.userId}`); this.getAllBundleFormsInfo(); }) } catch (errData) { let message = (errData as BusinessError).message; let errCode = (errData as BusinessError).code; console.log(`errData is errCode:${errCode} message:${message}`); } // Subscribe to bundle uninstall events. try { bundleMonitor.on('remove', (bundleChangeInfo) => { console.info(`bundleName : ${bundleChangeInfo.bundleName} userId : ${bundleChangeInfo.userId}`); this.getAllBundleFormsInfo(); }) } catch (errData) { let message = (errData as BusinessError).message; let errCode = (errData as BusinessError).code; console.log(`errData is errCode:${errCode} message:${message}`); } }).catch((error: BusinessError) => { console.error(`error, code: ${error.code}, message: ${error.message}`); }); } catch (error) { console.error(`catch error, code: ${(error as BusinessError).code}, message: ${(error as BusinessError).message}`); } } aboutToDisappear(): void { // Delete all widgets. this.formIds.forEach((id) => { console.log('delete all form') formHost.deleteForm(id); }); // Unsubscribe from bundle installation events. try { bundleMonitor.off('add'); } catch (errData) { let message = (errData as BusinessError).message; let errCode = (errData as BusinessError).code; console.log(`errData is errCode:${errCode} message:${message}`); } // Unsubscribe from bundle update events. try { bundleMonitor.off('update'); } catch (errData) { let message = (errData as BusinessError).message; let errCode = (errData as BusinessError).code; console.log(`errData is errCode:${errCode} message:${message}`); } // Unsubscribe from bundle uninstall events. try { bundleMonitor.off('remove'); } catch (errData) { let message = (errData as BusinessError).message; let errCode = (errData as BusinessError).code; console.log(`errData is errCode:${errCode} message:${message}`); } // Unsubscribe from events indicating that a widget becomes invisible and events indicating that a widget becomes visible. formObserver.off('notifyInvisible'); formObserver.off('notifyVisible'); } // Save the information of all widgets to formHapRecordMap. getAllBundleFormsInfo() { this.formCardHashMap.clear(); this.showFormPicker = false; let formHapRecordMap: HashMap = new HashMap(); this.formInfoRecord = []; formHost.getAllFormsInfo().then((formList: Array) => { console.log('getALlFormsInfo size:' + formList.length) for (let formItemInfo of formList) { let formBundleName = formItemInfo.bundleName; if (formHapRecordMap.hasKey(formBundleName)) { formHapRecordMap.get(formBundleName).push(formItemInfo) } else { let formInfoList: formInfo.FormInfo[] = [formItemInfo]; formHapRecordMap.set(formBundleName, formInfoList); } } for (let formBundle of formHapRecordMap.keys()) { let bundleFormInfo: TextCascadePickerRangeContent = { text: formBundle, children: [] } let bundleFormList: formInfo.FormInfo[] = formHapRecordMap.get(formBundle); bundleFormList.forEach((formItemInfo) => { let dimensionName = formHostSample.FORM_DIMENSIONS_MAP[formItemInfo.defaultDimension - 1]; bundleFormInfo.children?.push({ text: formItemInfo.name + '#' + dimensionName }); this.formCardHashMap.set(formBundle + "#" + formItemInfo.name + '#' + dimensionName, formItemInfo); }) this.formInfoRecord.push(bundleFormInfo); } this.formCardHashMap.forEach((formItem: formInfo.FormInfo) => { console.info(`formCardHashmap: ${JSON.stringify(formItem)}`); }) this.showFormPicker = true; }) } build() { Column() { Text(this.message) .fontSize(30) .fontWeight(FontWeight.Bold) Divider().vertical(false).color(Color.Black).lineCap(LineCapStyle.Butt).margin({ top: 10, bottom: 10 }) Row() { // Click to query information about all widgets. Button($r('app.string.inquiryForm')) .onClick(() => { this.getAllBundleFormsInfo(); }) // 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. Button($r('app.string.selectAddForm')) .enabled(this.showFormPicker) .onClick(() => { console.info("TextPickerDialog: show()") TextPickerDialog.show({ range: this.formInfoRecord, selected: this.pickDialogIndex, canLoop: false, disappearTextStyle: { color: Color.Red, font: { size: 10, weight: FontWeight.Lighter } }, textStyle: { color: Color.Black, font: { size: 12, weight: FontWeight.Normal } }, selectedTextStyle: { color: Color.Blue, font: { size: 12, weight: FontWeight.Bolder } }, onAccept: (result: TextPickerResult) => { this.currentFormKey = result.value[0] + "#" + result.value[1]; this.pickDialogIndex = result.index[0] console.info(`TextPickerDialog onAccept: ${this.currentFormKey}, ${this.pickDialogIndex}`); if (!this.formCardHashMap.hasKey(this.currentFormKey)) { console.error(`invalid formItemInfo by form key`) return; } this.showForm = true; this.focusFormInfo = this.formCardHashMap.get(this.currentFormKey); }, onCancel: () => { console.info("TextPickerDialog : onCancel()") }, onChange: (result: TextPickerResult) => { this.pickerBtnMsg = result.value[0] + '#' + result.value[1]; console.info("TextPickerDialog:onChange:" + this.pickerBtnMsg) } }) }) .margin({ left: 10 }) } .margin({ left: 10 }) Divider().vertical(false).color(Color.Black).lineCap(LineCapStyle.Butt).margin({ top: 10, bottom: 10 }) if(this.showForm){ Text(this.pickerBtnMsg) .margin({ top: 10, bottom: 10 }) } if (this.showForm) { Text('formId: ' + this.selectFormId) .margin({ top: 10, bottom: 10 }) // FormComponent FormComponent({ id: Number.parseInt(this.selectFormId), name: this.focusFormInfo.name, bundle: this.focusFormInfo.bundleName, ability: this.focusFormInfo.abilityName, module: this.focusFormInfo.moduleName, dimension: this.focusFormInfo.defaultDimension, temporary: false, }) .size({ width: formHostSample.FORM_SIZE[this.focusFormInfo.defaultDimension - 1][0], height: formHostSample.FORM_SIZE[this.focusFormInfo.defaultDimension - 1][1], }) .borderColor(Color.Black) .borderRadius(10) .borderWidth(1) .onAcquired((form: FormCallbackInfo) => { console.log(`onAcquired: ${JSON.stringify(form)}`) this.selectFormId = form.id.toString(); this.formIds.add(this.selectFormId); }) .onRouter(() => { console.log(`onRouter`) }) .onError((error) => { console.error(`onError: ${JSON.stringify(error)}`) this.showForm = false; }) .onUninstall((info: FormCallbackInfo) => { this.showForm = false; console.error(`onUninstall: ${JSON.stringify(info)}`) this.formIds.remove(this.selectFormId); }) // A select list that displays some formHost APIs Row() { Select([{ value: $r('app.string.deleteForm') }, { value: $r('app.string.updateForm') }, { value: $r('app.string.visibleForms') }, { value: $r('app.string.invisibleForms') }, { value: $r('app.string.enableFormsUpdate') }, { value: $r('app.string.disableFormsUpdate') }, ]) .selected(this.index) .value(this.operation) .font({ size: 16, weight: 500 }) .fontColor('#182431') .selectedOptionFont({ size: 16, weight: 400 }) .optionFont({ size: 16, weight: 400 }) .space(this.space) .arrowPosition(this.arrowPosition) .menuAlign(MenuAlignType.START, { dx: 0, dy: 0 }) .optionWidth(200) .optionHeight(300) .onSelect((index: number, text?: string | Resource) => { console.info('Select:' + index) this.index = index; if (text) { this.operation = text; } }) // Operate the widget based on what selected in the select list. Button($r('app.string.execute'), { type: ButtonType.Capsule }) .fontSize(16) .onClick(() => { switch (this.index) { case 0: try { formHost.deleteForm(this.selectFormId, (error: BusinessError) => { if (error) { console.error(`deleteForm error, code: ${error.code}, message: ${error.message}`); } else { console.log('formHost deleteForm success'); } }); } catch (error) { console.error(`deleteForm catch error, code: ${(error as BusinessError).code}, message: ${(error as BusinessError).message}`); } this.showForm = false; this.selectFormId = ''; break; case 1: try { formHost.requestForm(this.selectFormId, (error: BusinessError) => { if (error) { console.error(`requestForm error, code: ${error.code}, message: ${error.message}`); } }); } catch (error) { console.error(`requestForm catch error, code: ${(error as BusinessError).code}, message: ${(error as BusinessError).message}`); } break; case 2: try { formHost.notifyVisibleForms([this.selectFormId], (error: BusinessError) => { if (error) { console.error(`notifyVisibleForms error, code: ${error.code}, message: ${error.message}`); } else { console.info('notifyVisibleForms success'); } }); } catch (error) { console.error(`notifyVisibleForms catch error, code: ${(error as BusinessError).code}, message: ${(error as BusinessError).message}`); } break; case 3: try { formHost.notifyInvisibleForms([this.selectFormId], (error: BusinessError) => { if (error) { console.error(`notifyInvisibleForms error, code: ${error.code}, message: ${error.message}`); } else { console.info('notifyInvisibleForms success'); } }); } catch (error) { console.error(`notifyInvisibleForms catch error, code: ${(error as BusinessError).code}, message: ${(error as BusinessError).message}`); } break; case 4: try { formHost.enableFormsUpdate([this.selectFormId], (error: BusinessError) => { if (error) { console.error(`enableFormsUpdate error, code: ${error.code}, message: ${error.message}`); } }); } catch (error) { console.error(`enableFormsUpdate catch error, code: ${(error as BusinessError).code}, message: ${(error as BusinessError).message}`); } break; case 5: try { formHost.disableFormsUpdate([this.selectFormId], (error: BusinessError) => { if (error) { console.error(`disableFormsUpdate error, code: ${error.code}, message: ${error.message}`); } else { console.info('disableFormsUpdate success'); } }); } catch (error) { console.error(`disableFormsUpdate catch error, code: ${(error as BusinessError).code}, message: ${(error as BusinessError).message}`); } break; } }) } .margin({ top: 20, bottom: 10 }) } } } } ``` ![screenshot](./figures/widget-host-development-guide-2.jpeg)