1# Widget Development 2 3 4## Widget Overview 5 6A service widget (also called 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. 7 8A 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. 9 10Before you get started, it would be helpful if you have a basic understanding of the following concepts: 11 12- Widget host: an application that displays the widget content and controls the widget location. 13 14- Widget Manager: a resident agent that provides widget management features such as periodic widget updates. 15 16- Widget provider: an atomic service that provides the widget content to display and controls how widget components are laid out and how they interact with users. 17 18 19## Working Principles 20 21Figure 1 shows the working principles of the widget framework. 22 23**Figure 1** Widget framework working principles in the FA model 24 25 26 27The widget host consists of the following modules: 28 29- Widget usage: provides operations such as creating, deleting, or updating a widget. 30 31- Communication adapter: provided by the OpenHarmony SDK for communication with the Widget Manager. It sends widget-related operations to the Widget Manager. 32 33The Widget Manager consists of the following modules: 34 35- Periodic updater: starts a scheduled task based on the update policy to periodically update a widget after it is added to the Widget Manager. 36 37- Cache manager: caches view information of a widget after it is added to the Widget Manager to directly return the cached data when the widget is obtained next time. This reduces the latency greatly. 38 39- Lifecycle manager: suspends update when a widget is switched to the background or is blocked, and updates and/or clears widget data during upgrade and deletion. 40 41- Object manager: manages RPC objects of the widget host. It is used to verify requests from the widget host and process callbacks after the widget update. 42 43- Communication adapter: communicates with the widget host and provider through RPCs. 44 45The widget provider consists of the following modules: 46 47- Widget service: implemented by the widget provider developer to process requests on widget creation, update, and deletion, and to provide corresponding widget services. 48 49- Instance manager: implemented by the widget provider developer for persistent management of widget instances allocated by the Widget Manager. 50 51- Communication adapter: provided by the OpenHarmony SDK for communication with the Widget Manager. It pushes update data to the Widget Manager. 52 53> **NOTE** 54> 55> You only need to develop the widget provider. The system automatically handles the work of the widget host and Widget Manager. 56 57 58## Available APIs 59 60The **FormAbility** has the following APIs. 61 62| API| Description| 63| -------- | -------- | 64| onCreate(want: Want): formBindingData.FormBindingData | Called to notify the widget provider that a widget has been created.| 65| onCastToNormal(formId: string): void | Called to notify the widget provider that a temporary widget has been converted to a normal one.| 66| onUpdate(formId: string): void | Called to notify the widget provider that a widget has been updated.| 67| onVisibilityChange(newStatus: { [key: string]: number }): void | Called to notify the widget provider of the change in widget visibility.| 68| onEvent(formId: string, message: string): void | Called to instruct the widget provider to receive and process a widget event.| 69| onDestroy(formId: string): void | Called to notify the widget provider that a widget has been destroyed.| 70| onAcquireFormState?(want: Want): formInfo.FormState | Called to instruct the widget provider to receive the status query result of a widget.| 71| onShare?(formId: string): {[key: string]: any} | Called by the widget provider to receive shared widget data.| 72 73The **FormProvider** class has the following APIs. For details, see [FormProvider](../reference/apis/js-apis-app-form-formProvider.md). 74 75 76| API| Description| 77| -------- | -------- | 78| setFormNextRefreshTime(formId: string, minute: number, callback: AsyncCallback<void>): void;| Sets the next refresh time for a widget. This API uses an asynchronous callback to return the result.| 79| setFormNextRefreshTime(formId: string, minute: number): Promise<void>;| Sets the next refresh time for a widget. This API uses a promise to return the result.| 80| updateForm(formId: string, formBindingData: FormBindingData, callback: AsyncCallback<void>): void; | Updates a widget. This API uses an asynchronous callback to return the result.| 81| updateForm(formId: string, formBindingData: FormBindingData): Promise<void>; | Updates a widget. This API uses a promise to return the result.| 82 83 84The **FormBindingData** class has the following APIs. For details, see [FormBindingData](../reference/apis/js-apis-app-form-formBindingData.md). 85 86 87| API| Description| 88| -------- | -------- | 89| createFormBindingData(obj?: Object \ string): FormBindingData| | Creates a **FormBindingData** object.| 90 91 92## How to Develop 93 94The widget provider development based on the [FA model](fa-model-development-overview.md) involves the following key steps: 95 96- [Implementing Widget Lifecycle Callbacks](#implementing-widget-lifecycle-callbacks): Develop the **FormAbility** lifecycle callback functions. 97 98- [Configuring the Widget Configuration File](#configuring-the-widget-configuration-file): Configure the application configuration file **config.json**. 99 100- [Persistently Storing Widget Data](#persistently-storing-widget-data): Perform persistent management on widget information. 101 102- [Updating Widget Data](#updating-widget-data): Call **updateForm()** to update the information displayed on a widget. 103 104- [Developing the Widget UI Page](#developing-the-widget-ui-page): Use HML+CSS+JSON to develop a JS widget UI page. 105 106- [Developing Widget Events](#developing-widget-events): Add the router and message events for a widget. 107 108 109### Implementing Widget Lifecycle Callbacks 110 111To create a widget in the FA model, implement the widget lifecycle callbacks. Generate a widget template by referring to [Developing a Service Widget](https://developer.harmonyos.com/en/docs/documentation/doc-guides/ohos-development-service-widget-0000001263280425). 112 1131. Import related modules to **form.ts**. 114 115 ```ts 116 import formBindingData from '@ohos.app.form.formBindingData'; 117 import formInfo from '@ohos.app.form.formInfo'; 118 import formProvider from '@ohos.app.form.formProvider'; 119 import dataPreferences from '@ohos.data.preferences'; 120 import Want from '@ohos.app.ability.Want'; 121 ``` 122 1232. Implement the widget lifecycle callbacks in **form.ts**. 124 125 ```ts 126 class lifeCycle { 127 onCreate: (want: Want) => formBindingData.FormBindingData = (want) => ({ data: '' }) 128 onCastToNormal: (formId: string) => void = (formId) => {} 129 onUpdate: (formId: string) => void = (formId) => {} 130 onVisibilityChange: (newStatus: Record<string, number>) => void = (newStatus) => { 131 let obj: Record<string, number> = { 132 'test': 1 133 }; 134 return obj; 135 } 136 onEvent: (formId: string, message: string) => void = (formId, message) => {} 137 onDestroy: (formId: string) => void = (formId) => {} 138 onAcquireFormState?: (want: Want) => formInfo.FormState = (want) => (0) 139 onShare?: (formId: string) => Record<string, number | string | boolean | object | undefined | null> = (formId) => { 140 let obj: Record<string, number> = { 141 'test': 1 142 }; 143 return obj; 144 } 145 } 146 147 let obj: lifeCycle = { 148 onCreate(want: Want) { 149 console.info('FormAbility onCreate'); 150 // Called when the widget is created. The widget provider should return the widget data binding class. 151 let obj: Record<string, string> = { 152 "title": "titleOnCreate", 153 "detail": "detailOnCreate" 154 }; 155 let formData: formBindingData.FormBindingData = formBindingData.createFormBindingData(obj); 156 return formData; 157 }, 158 onCastToNormal(formId: string) { 159 // Called when the widget host converts the temporary widget into a normal one. The widget provider should do something to respond to the conversion. 160 console.info('FormAbility onCastToNormal'); 161 }, 162 onUpdate(formId: string) { 163 // Override this method to support scheduled updates, periodic updates, or updates requested by the widget host. 164 console.info('FormAbility onUpdate'); 165 let obj: Record<string, string> = { 166 "title": "titleOnUpdate", 167 "detail": "detailOnUpdate" 168 }; 169 let formData: formBindingData.FormBindingData = formBindingData.createFormBindingData(obj); 170 formProvider.updateForm(formId, formData).catch((error: Error) => { 171 console.info('FormAbility updateForm, error:' + JSON.stringify(error)); 172 }); 173 }, 174 onVisibilityChange(newStatus: Record<string, number>) { 175 // Called when the widget host initiates an event about visibility changes. The widget provider should do something to respond to the notification. This callback takes effect only for system applications. 176 console.info('FormAbility onVisibilityChange'); 177 }, 178 onEvent(formId: string, message: string) { 179 // If the widget supports event triggering, override this method and implement the trigger. 180 console.info('FormAbility onEvent'); 181 }, 182 onDestroy(formId: string) { 183 // Delete widget data. 184 console.info('FormAbility onDestroy'); 185 }, 186 onAcquireFormState(want: Want) { 187 console.info('FormAbility onAcquireFormState'); 188 return formInfo.FormState.READY; 189 }, 190 } 191 192 export default obj; 193 ``` 194 195> **NOTE** 196> 197> FormAbility cannot reside in the background. Therefore, continuous tasks cannot be processed in the widget lifecycle callbacks. 198 199### Configuring the Widget Configuration File 200 201The widget configuration file is named **config.json**. Find the **config.json** file for the widget and edit the file depending on your need. 202 203- The **js** module in the **config.json** file provides JavaScript resources of the widget. The internal structure is described as follows: 204 | Name| Description| Data Type| Initial Value Allowed| 205 | -------- | -------- | -------- | -------- | 206 | name | Name of a JavaScript component. The default value is **default**.| String| No| 207 | pages | Route information about all pages in the JavaScript component, including the page path and page name. The value is an array, in which each element represents a page. The first element in the array represents the home page of the JavaScript FA.| Array| No| 208 | window | Window-related configurations.| Object| Yes| 209 | type | Type of the JavaScript component. The value can be:<br>**normal**: indicates an application instance.<br>**form**: indicates a widget instance.| String| Yes (initial value: **normal**)| 210 | mode | Development mode of the JavaScript component.| Object| Yes (initial value: left empty)| 211 212 Example configuration: 213 214 215 ```json 216 "js": [{ 217 "name": "widget", 218 "pages": ["pages/index/index"], 219 "window": { 220 "designWidth": 720, 221 "autoDesignWidth": true 222 }, 223 "type": "form" 224 }] 225 ``` 226 227- The **abilities** module in the **config.json** file corresponds to **FormAbility** of the widget. The internal structure is described as follows: 228 | Name| Description| Data Type| Initial Value Allowed| 229 | -------- | -------- | -------- | -------- | 230 | name | Class name of a widget. The value is a string with a maximum of 127 bytes.| String| No| 231 | description | Description of the widget. The value can be a string or a resource index to descriptions in multiple languages. The value is a string with a maximum of 255 bytes.| String| Yes (initial value: left empty)| 232 | isDefault | Whether the widget is a default one. Each ability has only one default widget.<br>**true**: The widget is the default one.<br>**false**: The widget is not the default one.| Boolean| No| 233 | type | Type of the widget. The value can be:<br>**JS**: indicates a JavaScript-programmed widget.| String| No| 234 | colorMode | Color mode of the widget.<br>**auto**: The widget adopts the auto-adaptive color mode.<br>**dark**: The widget adopts the dark color mode.<br>**light**: The widget adopts the light color mode.| String| Yes (initial value: **auto**)| 235 | supportDimensions | Grid styles supported by the widget.<br>**1 * 2**: indicates a grid with one row and two columns.<br>**2 * 2**: indicates a grid with two rows and two columns.<br>**2 * 4**: indicates a grid with two rows and four columns.<br>**4 * 4**: indicates a grid with four rows and four columns.| String array| No| 236 | defaultDimension | Default grid style of the widget. The value must be available in the **supportDimensions** array of the widget.| String| No| 237 | updateEnabled | Whether the widget can be updated periodically.<br>**true**: The widget can be updated at a specified interval (**updateDuration**) or at the scheduled time (**scheduledUpdateTime**). **updateDuration** takes precedence over **scheduledUpdateTime**.<br>**false**: The widget cannot be updated periodically.| Boolean| No| 238 | scheduledUpdateTime | Scheduled time to update the widget. The value is in 24-hour format and accurate to minute.<br>**updateDuration** takes precedence over **scheduledUpdateTime**. If both are specified, the value specified by **updateDuration** is used.| String| Yes (initial value: **0:0**)| 239 | updateDuration | Interval to update the widget. The value is a natural number, in the unit of 30 minutes.<br>If the value is **0**, this field does not take effect.<br>If the value is a positive integer *N*, the interval is calculated by multiplying *N* and 30 minutes.<br>**updateDuration** takes precedence over **scheduledUpdateTime**. If both are specified, the value specified by **updateDuration** is used.| Number| Yes (initial value: **0**)| 240 | formConfigAbility | Link to a specific page of the application. The value is a URI.| String| Yes (initial value: left empty)| 241 | formVisibleNotify | Whether the widget is allowed to use the widget visibility notification.| String| Yes (initial value: left empty)| 242 | jsComponentName | Component name of the widget. The value is a string with a maximum of 127 bytes.| String| No| 243 | metaData | Metadata of the widget. This field contains the array of the **customizeData** field.| Object| Yes (initial value: left empty)| 244 | customizeData | Custom information about the widget.| Object array| Yes (initial value: left empty)| 245 246 Example configuration: 247 248 249 ```json 250 "abilities": [{ 251 "name": "FormAbility", 252 "description": "This is a FormAbility", 253 "formsEnabled": true, 254 "icon": "$media:icon", 255 "label": "$string:form_FormAbility_label", 256 "srcPath": "FormAbility", 257 "type": "service", 258 "srcLanguage": "ets", 259 "formsEnabled": true, 260 "formConfigAbility": "ability://com.example.entry.EntryAbility", 261 "forms": [{ 262 "colorMode": "auto", 263 "defaultDimension": "2*2", 264 "description": "This is a service widget.", 265 "formVisibleNotify": true, 266 "isDefault": true, 267 "jsComponentName": "widget", 268 "name": "widget", 269 "scheduledUpdateTime": "10:30", 270 "supportDimensions": ["2*2"], 271 "type": "JS", 272 "updateEnabled": true 273 }] 274 }] 275 ``` 276 277 278### Persistently Storing Widget Data 279 280A widget provider is usually started when it is needed to provide information about a widget. The Widget Manager supports multi-instance management and uses the widget ID to identify an instance. If the widget provider supports widget data modification, it must persistently store the data based on the widget ID, so that it can access the data of the target widget when obtaining, updating, or starting a widget. 281 282 283```ts 284const DATA_STORAGE_PATH: string = "/data/storage/el2/base/haps/form_store"; 285let storeFormInfo = async (formId: string, formName: string, tempFlag: boolean, context: Context) => { 286 // Only the widget ID (formId), widget name (formName), and whether the widget is a temporary one (tempFlag) are persistently stored. 287 let formInfo: Record<string, string | number | boolean> = { 288 "formName": formName, 289 "tempFlag": tempFlag, 290 "updateCount": 0 291 }; 292 try { 293 const storage = await dataPreferences.getPreferences(context, DATA_STORAGE_PATH); 294 // Put the widget information. 295 await storage.put(formId, JSON.stringify(formInfo)); 296 console.info(`storeFormInfo, put form info successfully, formId: ${formId}`); 297 await storage.flush(); 298 } catch (err) { 299 console.error(`failed to storeFormInfo, err: ${JSON.stringify(err as Error)}`); 300 } 301} 302 303... 304 onCreate(want: Want) { 305 console.info('FormAbility onCreate'); 306 307 if (want.parameters) { 308 let formId = String(want.parameters["ohos.extra.param.key.form_identity"]); 309 let formName = String(want.parameters["ohos.extra.param.key.form_name"]); 310 let tempFlag = Boolean(want.parameters["ohos.extra.param.key.form_temporary"]); 311 // Persistently store widget data for subsequent use, such as instance acquisition and update. 312 // Implement this API based on project requirements. 313 storeFormInfo(formId, formName, tempFlag, this.context); 314 } 315 316 let obj: Record<string, string> = { 317 "title": "titleOnCreate", 318 "detail": "detailOnCreate" 319 }; 320 let formData: formBindingData.FormBindingData = formBindingData.createFormBindingData(obj); 321 return formData; 322 } 323... 324``` 325 326You should override **onDestroy** to implement widget data deletion. 327 328 329```ts 330const DATA_STORAGE_PATH: string = "/data/storage/el2/base/haps/form_store"; 331let deleteFormInfo = async (formId: string, context: Context) => { 332 try { 333 const storage = await dataPreferences.getPreferences(context, DATA_STORAGE_PATH); 334 // Delete the widget information. 335 await storage.delete(formId); 336 console.info(`deleteFormInfo, del form info successfully, formId: ${formId}`); 337 await storage.flush(); 338 } catch (err) { 339 console.error(`failed to deleteFormInfo, err: ${JSON.stringify(err)}`); 340 } 341} 342 343... 344 onDestroy(formId: string) { 345 console.info('FormAbility onDestroy'); 346 // Delete the persistent widget instance data. 347 // Implement this API based on project requirements. 348 deleteFormInfo(formId, this.context); 349 } 350... 351``` 352 353For details about how to implement persistent data storage, see [Application Data Persistence Overview](../database/app-data-persistence-overview.md). 354 355The **Want** object passed in by the widget host to the widget provider contains a flag that specifies whether the requested widget is normal or temporary. 356 357- Normal widget: a widget persistently used by the widget host, for example, a widget added to the home screen. 358 359- Temporary widget: a widget temporarily used by the widget host, for example, the widget displayed when you swipe up on a widget application. 360 361Converting a temporary widget to a normal one: After you swipe up on a widget application, a temporary widget is displayed. If you touch the pin button on the widget, it is displayed as a normal widget on the home screen. 362 363Data 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. 364 365 366### Updating Widget Data 367 368When an application initiates a scheduled or periodic update, the application obtains the latest data and calls **updateForm()** to update the widget. 369 370 371```ts 372onUpdate(formId: string) { 373 // Override this method to support scheduled updates, periodic updates, or updates requested by the widget host. 374 console.info('FormAbility onUpdate'); 375 let obj: Record<string, string> = { 376 "title": "titleOnUpdate", 377 "detail": "detailOnUpdate" 378 }; 379 let formData: formBindingData.FormBindingData = formBindingData.createFormBindingData(obj); 380 // Call the updateForm() method to update the widget. Only the data passed through the input parameter is updated. Other information remains unchanged. 381 formProvider.updateForm(formId, formData).catch((error: Error) => { 382 console.info('FormAbility updateForm, error:' + JSON.stringify(error)); 383 }); 384} 385``` 386 387 388### Developing the Widget UI Page 389 390You can use the web-like paradigm (HML+CSS+JSON) to develop JS widget pages. This section describes how to develop a page shown below. 391 392 393 394> **NOTE** 395> 396> In the FA model, only the JavaScript-based web-like development paradigm is supported when developing the widget UI. 397 398- HML: uses web-like paradigm components to describe the widget page information. 399 400 ```html 401 <div class="container"> 402 <stack> 403 <div class="container-img"> 404 <image src="/common/widget.png" class="bg-img"></image> 405 </div> 406 <div class="container-inner"> 407 <text class="title">{{title}}</text> 408 <text class="detail_text" onclick="routerEvent">{{detail}}</text> 409 </div> 410 </stack> 411 </div> 412 ``` 413 414- CSS: defines style information about the web-like paradigm components in HML. 415 416 ```css 417 .container { 418 flex-direction: column; 419 justify-content: center; 420 align-items: center; 421 } 422 423 .bg-img { 424 flex-shrink: 0; 425 height: 100%; 426 } 427 428 .container-inner { 429 flex-direction: column; 430 justify-content: flex-end; 431 align-items: flex-start; 432 height: 100%; 433 width: 100%; 434 padding: 12px; 435 } 436 437 .title { 438 font-size: 19px; 439 font-weight: bold; 440 color: white; 441 text-overflow: ellipsis; 442 max-lines: 1; 443 } 444 445 .detail_text { 446 font-size: 16px; 447 color: white; 448 opacity: 0.66; 449 text-overflow: ellipsis; 450 max-lines: 1; 451 margin-top: 6px; 452 } 453 ``` 454 455- JSON: defines data and event interaction on the widget UI page. 456 457 ```json 458 { 459 "data": { 460 "title": "TitleDefault", 461 "detail": "TextDefault" 462 }, 463 "actions": { 464 "routerEvent": { 465 "action": "router", 466 "abilityName": "com.example.entry.EntryAbility", 467 "params": { 468 "message": "add detail" 469 } 470 } 471 } 472 } 473 ``` 474 475 476### Developing Widget Events 477 478You can set router and message events for components on a widget. The router event applies to ability redirection, and the message event applies to custom click events. The key steps are as follows: 479 4801. Set the **onclick** field in the HML file to **routerEvent** or **messageEvent**, depending on the **actions** settings in the JSON file. 481 4822. Set the router event. 483 - **action**: **"router"**, which indicates a router event. 484 - **abilityName**: name of the ability to redirect to (PageAbility component in the FA model and UIAbility component in the stage model). For example, the default UIAbility name created by DevEco Studio in the FA model is com.example.entry.EntryAbility. 485 - **params**: custom parameters passed to the target ability. Set them as required. The value can be obtained from **parameters** in **want** used for starting the target ability. For example, in the lifecycle function **onCreate** of the EntryAbility in the FA model, **featureAbility.getWant()** can be used to obtain **want** and its **parameters** field. 486 4873. Set the message event. 488 - **action**: **"message"**, which indicates a message event. 489 - **params**: custom parameters of the message event. Set them as required. The value can be obtained from **message** in the widget lifecycle function **onEvent**. 490 491The following is an example: 492 493- HML file: 494 495 ```html 496 <div class="container"> 497 <stack> 498 <div class="container-img"> 499 <image src="/common/widget.png" class="bg-img"></image> 500 </div> 501 <div class="container-inner"> 502 <text class="title" onclick="routerEvent">{{title}}</text> 503 <text class="detail_text" onclick="messageEvent">{{detail}}</text> 504 </div> 505 </stack> 506 </div> 507 ``` 508 509- CSS file: 510 511 ```css 512 .container { 513 flex-direction: column; 514 justify-content: center; 515 align-items: center; 516 } 517 518 .bg-img { 519 flex-shrink: 0; 520 height: 100%; 521 } 522 523 .container-inner { 524 flex-direction: column; 525 justify-content: flex-end; 526 align-items: flex-start; 527 height: 100%; 528 width: 100%; 529 padding: 12px; 530 } 531 532 .title { 533 font-size: 19px; 534 font-weight: bold; 535 color: white; 536 text-overflow: ellipsis; 537 max-lines: 1; 538 } 539 540 .detail_text { 541 font-size: 16px; 542 color: white; 543 opacity: 0.66; 544 text-overflow: ellipsis; 545 max-lines: 1; 546 margin-top: 6px; 547 } 548 ``` 549 550- JSON file: 551 552 ```json 553 { 554 "data": { 555 "title": "TitleDefault", 556 "detail": "TextDefault" 557 }, 558 "actions": { 559 "routerEvent": { 560 "action": "router", 561 "abilityName": "com.example.entry.EntryAbility", 562 "params": { 563 "message": "add detail" 564 } 565 }, 566 "messageEvent": { 567 "action": "message", 568 "params": { 569 "message": "add detail" 570 } 571 } 572 } 573 } 574 ``` 575