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  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 479