1# MVVM 2 3 4Rendering or re-rendering the UI based on state is complex, but critical to application performance. State data covers a collection of arrays, objects, or nested objects. In ArkUI, the Model-View-View Model (MVVM) pattern is leveraged for state management, where the state management module functions as the view model to bind data (part of model) to views. When data is changed, the views are updated. 5 6 7- Model: stores data and related logic. It represents data transferred between components or other related business logic. It is responsible for processing raw data. 8 9- View: typically represents the UI rendered by components decorated by \@Component. 10 11- View model: holds data stored in custom component state variables, LocalStorage, and AppStorage. 12 - A custom component renders the UI by executing its **build()** method or an \@Builder decorated method. In other words, the view model can render views. 13 - The view changes the view model through an event handler, that is, the change of the view model is driven by events. The view model provides the \@Watch callback method to listen for the change of state data. 14 - Any change of the view model must be synchronized back to the model to ensure the consistency between the view model and model, that is, the consistency of the application data. 15 - The view model structure should always be designed to adapt to the build and re-render of custom components. It is for this purpose that the model and view model are separated. 16 17 18A number of issues with UI construction and update arise from a poor view model design, which does not well support the rendering of custom components, or does not have a view model as a mediator, resulting in the custom component being forcibly adapted to the model. For example, a data model where an application directly reads data from the SQL database into the memory cannot well adapt to the rendering of custom components. In this scenario, the view model adaptation must be considered during application development. 19 20 21 22 23 24In the preceding example involving the SQL database, the application should be designed as follows: 25 26 27- Model: responsible for efficient database operations. 28 29- View model: responsible for efficient UI updates based on the ArkUI state management feature. 30 31- Converters/Adapters: responsible for conversion between the model and view model. 32 - Converters/Adapters can convert the model initially read from the database into a view model, and then initialize it. 33 - When the UI changes the view model through the event handler, the converters/adapters synchronize the updated data of the view model back to the model. 34 35 36Compared with the Model-View (MV) pattern, which forcibly fits the UI to the SQL database in this example, the MVVM pattern is more complex. The payback is a better UI performance with simplified UI design and implementation, thanks to its isolation of the view model layer. 37 38 39## View Model Data Sources 40 41 42The view model composes data from multiple top-level sources, such as variables decorated by \@State and \@Provide, LocalStorage, and AppStorage. Other decorators synchronize data with these data sources. The top-level data source to use depends on the extent to which the state needs to be shared between custom components as described below in ascending order by sharing scope: 43 44 45- \@State: component-level sharing, implemented through the named parameter mechanism. It is sharing between the parent component and child component by specifying parameters, for example, **CompA: ({ aProp: this.aProp })**. 46 47- \@Provide: component-level sharing, which is multi-level data sharing implemented by binding with \@Consume through a key. No parameter passing is involved during the sharing. 48 49- LocalStorage: page-level sharing, implemented by sharing LocalStorage instances in the current component tree through \@Entry. 50 51- AppStorage: application-level sharing, which is sharing of application-wide UI state bound with the application process. 52 53 54### State Data Sharing Through \@State 55 56 57A one- or two-way data synchronization relationship can be set up from an \@State decorated variable to an \@Prop, \@Link, or \@ObjectLink decorated variable. For details, see [\@State Decorator](arkts-state.md). 58 59 601. Use the \@State decorated variable **testNum** in the **Parent** root node as the view model data item. Pass **testNum** to the child components **LinkChild** and **Sibling**. 61 62 ```ts 63 // xxx.ets 64 @Entry 65 @Component 66 struct Parent { 67 @State @Watch("testNumChange1") testNum: number = 1; 68 69 testNumChange1(propName: string): void { 70 console.log(`Parent: testNumChange value ${this.testNum}`) 71 } 72 73 build() { 74 Column() { 75 LinkChild({ testNum: $testNum }) 76 Sibling({ testNum: $testNum }) 77 } 78 } 79 } 80 ``` 81 822. In **LinkChild** and **Sibling**, use \@Link to set up a two-way data synchronization with the data source of the **Parent** component. In this example, **LinkLinkChild** and **PropLinkChild** are created in **LinkChild**. 83 84 ```ts 85 @Component 86 struct Sibling { 87 @Link @Watch("testNumChange") testNum: number; 88 89 testNumChange(propName: string): void { 90 console.log(`Sibling: testNumChange value ${this.testNum}`); 91 } 92 93 build() { 94 Text(`Sibling: ${this.testNum}`) 95 } 96 } 97 98 @Component 99 struct LinkChild { 100 @Link @Watch("testNumChange") testNum: number; 101 102 testNumChange(propName: string): void { 103 console.log(`LinkChild: testNumChange value ${this.testNum}`); 104 } 105 106 build() { 107 Column() { 108 Button('incr testNum') 109 .onClick(() => { 110 console.log(`LinkChild: before value change value ${this.testNum}`); 111 this.testNum = this.testNum + 1 112 console.log(`LinkChild: after value change value ${this.testNum}`); 113 }) 114 Text(`LinkChild: ${this.testNum}`) 115 LinkLinkChild({ testNumGrand: $testNum }) 116 PropLinkChild({ testNumGrand: this.testNum }) 117 } 118 .height(200).width(200) 119 } 120 } 121 ``` 122 1233. Declare **LinkLinkChild** and **PropLinkChild** as follows. Use \@Prop in **PropLinkChild** to set up a one-way data synchronization with the data source of the **LinkChild** component. 124 125 ```ts 126 @Component 127 struct LinkLinkChild { 128 @Link @Watch("testNumChange") testNumGrand: number; 129 130 testNumChange(propName: string): void { 131 console.log(`LinkLinkChild: testNumGrand value ${this.testNumGrand}`); 132 } 133 134 build() { 135 Text(`LinkLinkChild: ${this.testNumGrand}`) 136 } 137 } 138 139 140 @Component 141 struct PropLinkChild { 142 @Prop @Watch("testNumChange") testNumGrand: number = 0; 143 144 testNumChange(propName: string): void { 145 console.log(`PropLinkChild: testNumGrand value ${this.testNumGrand}`); 146 } 147 148 build() { 149 Text(`PropLinkChild: ${this.testNumGrand}`) 150 .height(70) 151 .backgroundColor(Color.Red) 152 .onClick(() => { 153 this.testNumGrand += 1; 154 }) 155 } 156 } 157 ``` 158 159  160 161 When \@Link **testNum** in **LinkChild** changes: 162 163 1. The changes are first synchronized to its parent component **Parent**, and then from **Parent** to **Sibling**. 164 165 2. The changes are also synchronized to the child components **LinkLinkChild** and **PropLinkChild**. 166 167 Different from \@Provide, LocalStorage, and AppStorage, \@State is used with the following constraints: 168 169 - If you want to pass changes to a grandchild component, you must first pass the changes to the child component and then from the child component to the grandchild component. 170 - The changes can only be passed by specifying parameters of constructors, that is, through the named parameter mechanism CompA: ({ aProp: this.aProp }). 171 172 A complete code example is as follows: 173 174 175 ```ts 176 @Component 177 struct LinkLinkChild { 178 @Link @Watch("testNumChange") testNumGrand: number; 179 180 testNumChange(propName: string): void { 181 console.log(`LinkLinkChild: testNumGrand value ${this.testNumGrand}`); 182 } 183 184 build() { 185 Text(`LinkLinkChild: ${this.testNumGrand}`) 186 } 187 } 188 189 190 @Component 191 struct PropLinkChild { 192 @Prop @Watch("testNumChange") testNumGrand: number = 0; 193 194 testNumChange(propName: string): void { 195 console.log(`PropLinkChild: testNumGrand value ${this.testNumGrand}`); 196 } 197 198 build() { 199 Text(`PropLinkChild: ${this.testNumGrand}`) 200 .height(70) 201 .backgroundColor(Color.Red) 202 .onClick(() => { 203 this.testNumGrand += 1; 204 }) 205 } 206 } 207 208 209 @Component 210 struct Sibling { 211 @Link @Watch("testNumChange") testNum: number; 212 213 testNumChange(propName: string): void { 214 console.log(`Sibling: testNumChange value ${this.testNum}`); 215 } 216 217 build() { 218 Text(`Sibling: ${this.testNum}`) 219 } 220 } 221 222 @Component 223 struct LinkChild { 224 @Link @Watch("testNumChange") testNum: number; 225 226 testNumChange(propName: string): void { 227 console.log(`LinkChild: testNumChange value ${this.testNum}`); 228 } 229 230 build() { 231 Column() { 232 Button('incr testNum') 233 .onClick(() => { 234 console.log(`LinkChild: before value change value ${this.testNum}`); 235 this.testNum = this.testNum + 1 236 console.log(`LinkChild: after value change value ${this.testNum}`); 237 }) 238 Text(`LinkChild: ${this.testNum}`) 239 LinkLinkChild({ testNumGrand: $testNum }) 240 PropLinkChild({ testNumGrand: this.testNum }) 241 } 242 .height(200).width(200) 243 } 244 } 245 246 247 @Entry 248 @Component 249 struct Parent { 250 @State @Watch("testNumChange1") testNum: number = 1; 251 252 testNumChange1(propName: string): void { 253 console.log(`Parent: testNumChange value ${this.testNum}`) 254 } 255 256 build() { 257 Column() { 258 LinkChild({ testNum: $testNum }) 259 Sibling({ testNum: $testNum }) 260 } 261 } 262 } 263 ``` 264 265 266### State Data Sharing Through \@Provide 267 268\@Provide decorated variables can share state data with any descendant component that uses \@Consume to create a two-way synchronization. For details, see [\@Provide and \@Consume Decorators](arkts-provide-and-consume.md). 269 270This \@Provide-\@Consume pattern is more convenient than the \@State-\@Link-\@Link pattern in terms of passing changes from a parent component to a grandchild component. It is suitable for sharing state data in a single page UI component tree. 271 272In the \@Provide-\@Consume pattern, changes are passed by binding \@Consume to \@Provide in the ancestor component through a key, instead of by specifying parameters in the constructor. 273 274The following example uses the \@Provide-\@Consume pattern to pass changes from a parent component to a grandchild component: 275 276 277```ts 278@Component 279struct LinkLinkChild { 280 @Consume @Watch("testNumChange") testNum: number; 281 282 testNumChange(propName: string): void { 283 console.log(`LinkLinkChild: testNum value ${this.testNum}`); 284 } 285 286 build() { 287 Text(`LinkLinkChild: ${this.testNum}`) 288 } 289} 290 291@Component 292struct PropLinkChild { 293 @Prop @Watch("testNumChange") testNumGrand: number = 0; 294 295 testNumChange(propName: string): void { 296 console.log(`PropLinkChild: testNumGrand value ${this.testNumGrand}`); 297 } 298 299 build() { 300 Text(`PropLinkChild: ${this.testNumGrand}`) 301 .height(70) 302 .backgroundColor(Color.Red) 303 .onClick(() => { 304 this.testNumGrand += 1; 305 }) 306 } 307} 308 309@Component 310struct Sibling { 311 @Consume @Watch("testNumChange") testNum: number; 312 313 testNumChange(propName: string): void { 314 console.log(`Sibling: testNumChange value ${this.testNum}`); 315 } 316 317 build() { 318 Text(`Sibling: ${this.testNum}`) 319 } 320} 321 322@Component 323struct LinkChild { 324 @Consume @Watch("testNumChange") testNum: number; 325 326 testNumChange(propName: string): void { 327 console.log(`LinkChild: testNumChange value ${this.testNum}`); 328 } 329 330 build() { 331 Column() { 332 Button('incr testNum') 333 .onClick(() => { 334 console.log(`LinkChild: before value change value ${this.testNum}`); 335 this.testNum = this.testNum + 1 336 console.log(`LinkChild: after value change value ${this.testNum}`); 337 }) 338 Text(`LinkChild: ${this.testNum}`) 339 LinkLinkChild({ /* empty */ }) 340 PropLinkChild({ testNumGrand: this.testNum }) 341 } 342 .height(200).width(200) 343 } 344} 345 346@Entry 347@Component 348struct Parent { 349 @Provide @Watch("testNumChange1") testNum: number = 1; 350 351 testNumChange1(propName: string): void { 352 console.log(`Parent: testNumChange value ${this.testNum}`) 353 } 354 355 build() { 356 Column() { 357 LinkChild({ /* empty */ }) 358 Sibling({ /* empty */ }) 359 } 360 } 361} 362``` 363 364 365### One- or Two-Way Synchronization for Properties in LocalStorage Instances 366 367You can use \@LocalStorageLink to set up a one-way synchronization for a property in a LocalStorage instance, or use \@LocalStorageProp to set up a two-way synchronization. A LocalStorage instance can be regarded as a map of the \@State decorated variables. For details, see [LocalStorage](arkts-localstorage.md). 368 369A LocalStorage instance can be shared on several pages of an ArkUI application. In this way, state can be shared across pages of an application using \@LocalStorageLink, \@LocalStorageProp, and LocalStorage. 370 371Below is an example. 372 3731. Create a LocalStorage instance and inject it into the root node through \@Entry(storage). 374 3752. When the \@LocalStorageLink("testNum") variable is initialized in the **Parent** component, the **testNum** property, with the initial value set to **1**, is created in the LocalStorage instance, that is, \@LocalStorageLink("testNum") testNum: number = 1. 376 3773. In the child components, use \@LocalStorageLink or \@LocalStorageProp to bind the same property name key to pass data. 378 379The LocalStorage instance can be considered as a map of the \@State decorated variables, and the property name is the key in the map. 380 381The synchronization between \@LocalStorageLink and the corresponding property in LocalStorage is two-way, the same as that between \@State and \@Link. 382 383The following figure shows the flow of component state update. 384 385 386 387 388```ts 389@Component 390struct LinkLinkChild { 391 @LocalStorageLink("testNum") @Watch("testNumChange") testNum: number = 1; 392 393 testNumChange(propName: string): void { 394 console.log(`LinkLinkChild: testNum value ${this.testNum}`); 395 } 396 397 build() { 398 Text(`LinkLinkChild: ${this.testNum}`) 399 } 400} 401 402@Component 403struct PropLinkChild { 404 @LocalStorageProp("testNum") @Watch("testNumChange") testNumGrand: number = 1; 405 406 testNumChange(propName: string): void { 407 console.log(`PropLinkChild: testNumGrand value ${this.testNumGrand}`); 408 } 409 410 build() { 411 Text(`PropLinkChild: ${this.testNumGrand}`) 412 .height(70) 413 .backgroundColor(Color.Red) 414 .onClick(() => { 415 this.testNumGrand += 1; 416 }) 417 } 418} 419 420@Component 421struct Sibling { 422 @LocalStorageLink("testNum") @Watch("testNumChange") testNum: number = 1; 423 424 testNumChange(propName: string): void { 425 console.log(`Sibling: testNumChange value ${this.testNum}`); 426 } 427 428 build() { 429 Text(`Sibling: ${this.testNum}`) 430 } 431} 432 433@Component 434struct LinkChild { 435 @LocalStorageLink("testNum") @Watch("testNumChange") testNum: number = 1; 436 437 testNumChange(propName: string): void { 438 console.log(`LinkChild: testNumChange value ${this.testNum}`); 439 } 440 441 build() { 442 Column() { 443 Button('incr testNum') 444 .onClick(() => { 445 console.log(`LinkChild: before value change value ${this.testNum}`); 446 this.testNum = this.testNum + 1 447 console.log(`LinkChild: after value change value ${this.testNum}`); 448 }) 449 Text(`LinkChild: ${this.testNum}`) 450 LinkLinkChild({ /* empty */ }) 451 PropLinkChild({ /* empty */ }) 452 } 453 .height(200).width(200) 454 } 455} 456 457// Create a LocalStorage instance to hold data. 458const storage = new LocalStorage(); 459@Entry(storage) 460@Component 461struct Parent { 462 @LocalStorageLink("testNum") @Watch("testNumChange1") testNum: number = 1; 463 464 testNumChange1(propName: string): void { 465 console.log(`Parent: testNumChange value ${this.testNum}`) 466 } 467 468 build() { 469 Column() { 470 LinkChild({ /* empty */ }) 471 Sibling({ /* empty */ }) 472 } 473 } 474} 475``` 476 477 478### One- or Two-Way Synchronization for Properties in AppStorage 479 480AppStorage is a singleton of LocalStorage. ArkUI creates this instance when an application is started and uses \@StorageLink and \@StorageProp to implement data sharing across pages. The usage of AppStorage is similar to that of LocalStorage. 481 482You can also use PersistentStorage to persist specific properties in AppStorage to files on the local disk. In this way, \@StorageLink and \@StorageProp decorated properties can restore upon application re-start to the values as they were when the application was closed. For details, see [PersistentStorage](arkts-persiststorage.md). 483 484An example is as follows: 485 486 487```ts 488@Component 489struct LinkLinkChild { 490 @StorageLink("testNum") @Watch("testNumChange") testNum: number = 1; 491 492 testNumChange(propName: string): void { 493 console.log(`LinkLinkChild: testNum value ${this.testNum}`); 494 } 495 496 build() { 497 Text(`LinkLinkChild: ${this.testNum}`) 498 } 499} 500 501@Component 502struct PropLinkChild { 503 @StorageProp("testNum") @Watch("testNumChange") testNumGrand: number = 1; 504 505 testNumChange(propName: string): void { 506 console.log(`PropLinkChild: testNumGrand value ${this.testNumGrand}`); 507 } 508 509 build() { 510 Text(`PropLinkChild: ${this.testNumGrand}`) 511 .height(70) 512 .backgroundColor(Color.Red) 513 .onClick(() => { 514 this.testNumGrand += 1; 515 }) 516 } 517} 518 519@Component 520struct Sibling { 521 @StorageLink("testNum") @Watch("testNumChange") testNum: number = 1; 522 523 testNumChange(propName: string): void { 524 console.log(`Sibling: testNumChange value ${this.testNum}`); 525 } 526 527 build() { 528 Text(`Sibling: ${this.testNum}`) 529 } 530} 531 532@Component 533struct LinkChild { 534 @StorageLink("testNum") @Watch("testNumChange") testNum: number = 1; 535 536 testNumChange(propName: string): void { 537 console.log(`LinkChild: testNumChange value ${this.testNum}`); 538 } 539 540 build() { 541 Column() { 542 Button('incr testNum') 543 .onClick(() => { 544 console.log(`LinkChild: before value change value ${this.testNum}`); 545 this.testNum = this.testNum + 1 546 console.log(`LinkChild: after value change value ${this.testNum}`); 547 }) 548 Text(`LinkChild: ${this.testNum}`) 549 LinkLinkChild({ /* empty */ 550 }) 551 PropLinkChild({ /* empty */ 552 }) 553 } 554 .height(200).width(200) 555 } 556} 557 558 559@Entry 560@Component 561struct Parent { 562 @StorageLink("testNum") @Watch("testNumChange1") testNum: number = 1; 563 564 testNumChange1(propName: string): void { 565 console.log(`Parent: testNumChange value ${this.testNum}`) 566 } 567 568 build() { 569 Column() { 570 LinkChild({ /* empty */ 571 }) 572 Sibling({ /* empty */ 573 }) 574 } 575 } 576} 577``` 578 579 580## Nested View Model 581 582 583In most cases, view model data items are of complex types, such as arrays of objects, nested objects, or their combinations. In nested scenarios, you can use \@Observed and \@Prop or \@ObjectLink to observe changes. 584 585 586### \@Prop and \@ObjectLink Nested Data Structures 587 588When possible, design a separate custom component to render each array or object. In this case, an object array or nested object (which is an object whose property is an object) requires two custom components: one for rendering an external array/object, and the other for rendering a class object nested within the array/object. For variables decorated by \@State, \@Prop, \@Link, and \@ObjectLink, only changes at the first layer can be observed. 589 590- For a class: 591 - Value assignment changes can be observed: this.obj=new ClassObj(...) 592 - Object property changes can be observed: this.obj.a=new ClassA(...) 593 - Property changes at a deeper layer cannot be observed: this.obj.a.b = 47 594 595- For an array: 596 - The overall value assignment of the array can be observed: this.arr=[...] 597 - The deletion, insertion, and replacement of data items can be observed: this.arr[1] = new ClassA(), this.arr.pop(), this.arr.push(new ClassA(...)), this.arr.sort(...) 598 - Array changes at a deeper layer cannot be observed: this.arr[1].b = 47 599 600To observe changes of nested objects inside a class, use \@ObjectLink or \@Prop. \@ObjectLink is preferred, which initializes itself through a reference to an internal property of a nested object. \@Prop initializes itself through a deep copy of the nested object to implement one-way synchronization. The reference copy of \@ObjectLink significantly outperforms the deep copy of \@Prop. 601 602\@ObjectLink or \@Prop can be used to store nested objects of a class. This class must be decorated with \@Observed. Otherwise, its property changes will not trigger UI re-rendering. \@Observed implements a custom constructor for its decorated class. This constructor creates an instance of a class and uses the ES6 proxy wrapper (implemented by the ArkUI framework) to intercept all get and set operations on the decorated class property. "Set" observes the property value. When value assignment occurs, the ArkUI framework is notified of the update. "Get" collects UI components that depend on this state variable to minimize UI re-rendering. 603 604In the nested scenario, use the \@Observed decorator as follows: 605 606- If the nested data is a class, directly decorate it with \@Observed. 607 608- If the nested data is an array, you can observe the array change in the following way: 609 610 ```ts 611 @Observed class ObservedArray<T> extends Array<T> { 612 constructor(args: T[]) { 613 if (args instanceof Array) { 614 super(...args); 615 } else { 616 super(args) 617 } 618 } 619 /* otherwise empty */ 620 } 621 ``` 622 623 The view model is the outer class. 624 625 626 ```ts 627 class Outer { 628 innerArrayProp : ObservedArray<string> = []; 629 ... 630 } 631 ``` 632 633 634### Differences Between \@Prop and \@ObjectLink in Nested Data Structures 635 636In the following example: 637 638- The parent component **ViewB** renders \@State arrA: Array\<ClassA>. \@State can observe the allocation of new arrays, and insertion, deletion, and replacement of array items. 639 640- The child component **ViewA** renders each object of **ClassA**. 641 642- With \@ObjectLink a: ClassA: 643 644 - When \@Observed ClassA is used, the changes of **ClassA** objects nested in the array can be observed. 645 646 - When \@Observed ClassA is not used, 647 this.arrA[Math.floor(this.arrA.length/2)].c=10 in **ViewB** cannot be observed, and therefore the **ViewA** component will not be updated. 648 649 For the first and second array items in the array, both of them initialize two **ViewA** objects and render the same **ViewA** instance. When this.a.c += 1; is assigned to a property in **ViewA**, another **ViewA** initialized from the same **ClassA** is not re-rendered. 650 651 652 653 654```ts 655let NextID: number = 1; 656 657// Use the class decorator @Observed to decorate ClassA. 658@Observed 659class ClassA { 660 public id: number; 661 public c: number; 662 663 constructor(c: number) { 664 this.id = NextID++; 665 this.c = c; 666 } 667} 668 669@Component 670struct ViewA { 671 @ObjectLink a: ClassA; 672 label: string = "ViewA1"; 673 674 build() { 675 Row() { 676 Button(`ViewA [${this.label}] this.a.c= ${this.a.c} +1`) 677 .onClick(() => { 678 // Change the object property. 679 this.a.c += 1; 680 }) 681 } 682 } 683} 684 685@Entry 686@Component 687struct ViewB { 688 @State arrA: ClassA[] = [new ClassA(0), new ClassA(0)]; 689 690 build() { 691 Column() { 692 ForEach(this.arrA, 693 (item: ClassA) => { 694 ViewA({ label: `#${item.id}`, a: item }) 695 }, 696 (item: ClassA): string => { return item.id.toString(); } 697 ) 698 699 Divider().height(10) 700 701 if (this.arrA.length) { 702 ViewA({ label: `ViewA this.arrA[first]`, a: this.arrA[0] }) 703 ViewA({ label: `ViewA this.arrA[last]`, a: this.arrA[this.arrA.length-1] }) 704 } 705 706 Divider().height(10) 707 708 Button(`ViewB: reset array`) 709 .onClick(() => { 710 // Replace the entire array, which will be observed by @State this.arrA. 711 this.arrA = [new ClassA(0), new ClassA(0)]; 712 }) 713 Button(`array push`) 714 .onClick(() => { 715 // Insert data into the array, which will be observed by @State this.arrA. 716 this.arrA.push(new ClassA(0)) 717 }) 718 Button(`array shift`) 719 .onClick(() => { 720 // Remove data from the array, which will be observed by @State this.arrA. 721 this.arrA.shift() 722 }) 723 Button(`ViewB: chg item property in middle`) 724 .onClick(() => { 725 // Replace an item in the array, which will be observed by @State this.arrA. 726 this.arrA[Math.floor(this.arrA.length / 2)] = new ClassA(11); 727 }) 728 Button(`ViewB: chg item property in middle`) 729 .onClick(() => { 730 // Change property c of an item in the array, which will be observed by @ObjectLink in ViewA. 731 this.arrA[Math.floor(this.arrA.length / 2)].c = 10; 732 }) 733 } 734 } 735} 736``` 737 738In **ViewA**, replace \@ObjectLink with \@Prop. 739 740 741```ts 742@Component 743struct ViewA { 744 745 @Prop a: ClassA = new ClassA(0); 746 label : string = "ViewA1"; 747 748 build() { 749 Row() { 750 Button(`ViewA [${this.label}] this.a.c= ${this.a.c} +1`) 751 .onClick(() => { 752 // Change the object property. 753 this.a.c += 1; 754 }) 755 } 756 } 757} 758``` 759 760When \@ObjectLink is used, if you click the first or second item of the array, the following two **ViewA** instances change synchronously. 761 762Unlike \@ObjectLink, \@Prop sets up a one-way data synchronization. Clicking the button in **ViewA** triggers only the re-rendering of the button itself and is not propagated to other **ViewA** instances. **ClassA** in **ViewA** is only a copy, not an object of its parent component \@State arrA : Array\<ClassA>, nor a **ClassA** instance of any other **ViewA**. As a result, though on the surface, the array and **ViewA** have the same object passed in, two irrelevant objects are used for rendering on the UI. 763 764Note the differences between \@Prop and \@ObjectLink: \@ObjectLink decorated variables are readable only and cannot be assigned values, whereas \@Prop decorated variables can be assigned values. 765 766- \@ObjectLink implements two-way data synchronization because it is initialized through a reference to the data source. 767 768- \@Prop implements one-way data synchronization and requires a deep copy of the data source. 769 770- To assign a new object to \@Prop is to overwrite the local value. However, for \@ObjectLink, to assign a new object is to update the array item or class property in the data source, which is not possible in TypeScript/JavaScript. 771 772 773## Example 774 775 776The following example discusses the design of nested view models, especially how a custom component renders a nested object. This scenario is common in real-world application development. 777 778 779Let's develop a phonebook application to implement the following features: 780 781 782- Display the phone numbers of contacts and the local device ("Me"). 783 784- You can select a contact and edit its information, including the phone number and address. 785 786- When you update contact information, the changes are saved only after you click **Save Changes**. 787 788- You can click **Delete Contact** to delete a contact from the contacts list. 789 790 791In this example, the view model needs to include the following: 792 793 794- **AddressBook** (class) 795 - **me**: stores a **Person** class. 796 - **contacts**: stores a **Person** class array. 797 798 799The **AddressBook** class is declared as follows: 800 801```ts 802export class AddressBook { 803 me: Person; 804 contacts: ObservedArray<Person>; 805 806 constructor(me: Person, contacts: Person[]) { 807 this.me = me; 808 this.contacts = new ObservedArray<Person>(contacts); 809 } 810} 811``` 812 813 814- Person (class) 815 - name : string 816 - address : Address 817 - phones: ObservedArray\<string>; 818 - Address (class) 819 - street : string 820 - zip : number 821 - city : string 822 823 824The **Address** class is declared as follows: 825 826```ts 827@Observed 828export class Address { 829 street: string; 830 zip: number; 831 city: string; 832 833 constructor(street: string, 834 zip: number, 835 city: string) { 836 this.street = street; 837 this.zip = zip; 838 this.city = city; 839 } 840} 841``` 842 843 844The **Person** class is declared as follows: 845 846```ts 847let nextId = 0; 848 849@Observed 850export class Person { 851 id_: string; 852 name: string; 853 address: Address; 854 phones: ObservedArray<string>; 855 856 constructor(name: string, 857 street: string, 858 zip: number, 859 city: string, 860 phones: string[]) { 861 this.id_ = `${nextId}`; 862 nextId++; 863 this.name = name; 864 this.address = new Address(street, zip, city); 865 this.phones = new ObservedArray<string>(phones); 866 } 867} 868``` 869 870 871Note that **phones** is a nested property. To observe its change, you need to extend the array to an **ObservedArray** class and decorate it with \@Observed. The **ObservedArray** class is declared as follows: 872 873```ts 874@Observed 875export class ObservedArray<T> extends Array<T> { 876 constructor(args: T[]) { 877 console.log(`ObservedArray: ${JSON.stringify(args)} `) 878 if (args instanceof Array) { 879 super(...args); 880 } else { 881 super(args) 882 } 883 } 884} 885``` 886 887 888- **selected**: reference to **Person**. 889 890 891The update process is as follows: 892 893 8941. Initialize all data in the root node **PageEntry**, and establish two-way data synchronization between **me** and **contacts** and its child component **AddressBookView**. The default value of **selectedPerson** is **me**. Note that **selectedPerson** is not data in the **PageEntry** data source, but a reference to a **Person** object in the data source. 895 **PageEntry** and **AddressBookView** are declared as follows: 896 897 898 ```ts 899 @Component 900 struct AddressBookView { 901 902 @ObjectLink me : Person; 903 @ObjectLink contacts : ObservedArray<Person>; 904 @State selectedPerson: Person = new Person("", "", 0, "", []); 905 906 aboutToAppear() { 907 this.selectedPerson = this.me; 908 } 909 910 build() { 911 Flex({ direction: FlexDirection.Column, justifyContent: FlexAlign.Start}) { 912 Text("Me:") 913 PersonView({ 914 person: this.me, 915 phones: this.me.phones, 916 selectedPerson: this.selectedPerson 917 }) 918 919 Divider().height(8) 920 921 ForEach(this.contacts, (contact: Person) => { 922 PersonView({ 923 person: contact, 924 phones: contact.phones as ObservedArray<string>, 925 selectedPerson: this.selectedPerson 926 }) 927 }, 928 (contact: Person): string => { return contact.id_; } 929 ) 930 931 Divider().height(8) 932 933 Text("Edit:") 934 PersonEditView({ 935 selectedPerson: this.selectedPerson, 936 name: this.selectedPerson.name, 937 address: this.selectedPerson.address, 938 phones: this.selectedPerson.phones 939 }) 940 } 941 .borderStyle(BorderStyle.Solid).borderWidth(5).borderColor(0xAFEEEE).borderRadius(5) 942 } 943 } 944 945 @Entry 946 @Component 947 struct PageEntry { 948 @Provide addrBook: AddressBook = new AddressBook( 949 new Person("Gigi", "Itamerenkatu 9", 180, "Helsinki", ["18*********", "18*********", "18*********"]), 950 [ 951 new Person("Oly", "Itamerenkatu 9", 180, "Helsinki", ["18*********", "18*********"]), 952 new Person("Sam", "Itamerenkatu 9", 180, "Helsinki", ["18*********", "18*********"]), 953 new Person("Vivi", "Itamerenkatu 9", 180, "Helsinki", ["18*********", "18*********"]), 954 ]); 955 956 build() { 957 Column() { 958 AddressBookView({ 959 me: this.addrBook.me, 960 contacts: this.addrBook.contacts, 961 selectedPerson: this.addrBook.me 962 }) 963 } 964 } 965 } 966 ``` 967 9682. **PersonView** is the view that shows a contact name and preferred phone number in the phonebook. When you select a contact (person), that contact is highlighted and needs to be synchronized back to the **selectedPerson** of the parent component **AddressBookView**. In this case, two-way data synchronization needs to be established through \@Link. 969 **PersonView** is declared as follows: 970 971 972 ```ts 973 // Display the contact name and preferred phone number. 974 // To update the phone number, @ObjectLink person and @ObjectLink phones are required. 975 // this.person.phones[0] cannot be used to display the preferred phone number because @ObjectLink person only proxies the Person property and cannot observe the changes inside the array. 976 // Trigger the onClick event to update selectedPerson. 977 @Component 978 struct PersonView { 979 980 @ObjectLink person : Person; 981 @ObjectLink phones : ObservedArray<string>; 982 983 @Link selectedPerson : Person; 984 985 build() { 986 Flex({ direction: FlexDirection.Row, justifyContent: FlexAlign.SpaceBetween }) { 987 Text(this.person.name) 988 if (this.phones.length > 0) { 989 Text(this.phones[0]) 990 } 991 } 992 .height(55) 993 .backgroundColor(this.selectedPerson.name == this.person.name ? "#ffa0a0" : "#ffffff") 994 .onClick(() => { 995 this.selectedPerson = this.person; 996 }) 997 } 998 } 999 ``` 1000 10013. The information about the selected contact (person) is displayed in the **PersonEditView** object. The data synchronization for the **PersonEditView** can be implemented as follows: 1002 1003 - When the user's keyboard input is received in the Edit state through the **Input.onChange** callback event, the change should be reflected in the current **PersonEditView**, but does not need to be synchronized back to the data source before **Save Changes** is clicked. In this case, \@Prop is used to make a deep copy of the information about the current contact (person). 1004 1005 - Through \@Link **seletedPerson: Person**, **PersonEditView** establishes two-way data synchronization with **selectedPerson** of **AddressBookView**. When you click **Save Changes**, the change to \@Prop is assigned to \@Link **seletedPerson: Person**. In this way, the data is synchronized back to the data source. 1006 1007 - In **PersonEditView**, \@Consume **addrBook: AddressBook** is used to set up two-way synchronization with the root node **PageEntry**. When you delete a contact on the **PersonEditView** page, the deletion is directly synchronized to **PageEntry**, which then instructs **AddressBookView** to update the contracts list page. **PersonEditView** is declared as follows: 1008 1009 ```ts 1010 // Render the information about the contact (person). 1011 // The @Prop decorated variable makes a deep copy from the parent component AddressBookView and retains the changes locally. The changes of TextInput apply only to the local copy. 1012 // Click Save Changes to copy all data to @Link through @Prop and synchronize the data to other components. 1013 @Component 1014 struct PersonEditView { 1015 1016 @Consume addrBook : AddressBook; 1017 1018 /* Reference pointing to selectedPerson in the parent component. */ 1019 @Link selectedPerson: Person; 1020 1021 /* Make changes on the local copy until you click Save Changes. */ 1022 @Prop name: string = ""; 1023 @Prop address : Address = new Address("", 0, ""); 1024 @Prop phones : ObservedArray<string> = []; 1025 1026 selectedPersonIndex() : number { 1027 return this.addrBook.contacts.findIndex((person: Person) => person.id_ == this.selectedPerson.id_); 1028 } 1029 1030 build() { 1031 Column() { 1032 TextInput({ text: this.name}) 1033 .onChange((value) => { 1034 this.name = value; 1035 }) 1036 TextInput({text: this.address.street}) 1037 .onChange((value) => { 1038 this.address.street = value; 1039 }) 1040 1041 TextInput({text: this.address.city}) 1042 .onChange((value) => { 1043 this.address.city = value; 1044 }) 1045 1046 TextInput({text: this.address.zip.toString()}) 1047 .onChange((value) => { 1048 const result = Number.parseInt(value); 1049 this.address.zip= Number.isNaN(result) ? 0 : result; 1050 }) 1051 1052 if (this.phones.length > 0) { 1053 ForEach(this.phones, 1054 (phone: ResourceStr, index?:number) => { 1055 TextInput({ text: phone }) 1056 .width(150) 1057 .onChange((value) => { 1058 console.log(`${index}. ${value} value has changed`) 1059 this.phones[index!] = value; 1060 }) 1061 }, 1062 (phone: ResourceStr, index?:number) => `${index}` 1063 ) 1064 } 1065 1066 Flex({ direction: FlexDirection.Row, justifyContent: FlexAlign.SpaceBetween }) { 1067 Text("Save Changes") 1068 .onClick(() => { 1069 // Assign the updated value of the local copy to the reference pointing to selectedPerson in the parent component. 1070 // Do not create new objects. Modify the properties of the existing objects instead. 1071 this.selectedPerson.name = this.name; 1072 this.selectedPerson.address = new Address(this.address.street, this.address.zip, this.address.city) 1073 this.phones.forEach((phone : string, index : number) => { this.selectedPerson.phones[index] = phone } ); 1074 }) 1075 if (this.selectedPersonIndex()!=-1) { 1076 Text("Delete Contact") 1077 .onClick(() => { 1078 let index = this.selectedPersonIndex(); 1079 console.log(`delete contact at index ${index}`); 1080 1081 // Delete the current contact. 1082 this.addrBook.contacts.splice(index, 1); 1083 1084 // Delete the current selectedPerson. The selected contact is then changed to the contact immediately before the deleted contact. 1085 index = (index < this.addrBook.contacts.length) ? index : index-1; 1086 1087 // If all contracts are deleted, the me object is selected. 1088 this.selectedPerson = (index>=0) ? this.addrBook.contacts[index] : this.addrBook.me; 1089 }) 1090 } 1091 } 1092 1093 } 1094 } 1095 } 1096 ``` 1097 1098 Pay attention to the following differences between \@ObjectLink and \@Link: 1099 1100 1. To implement two-way data synchronization with the parent component, you need to use \@ObjectLink, instead of \@Link, to decorate **me: Person** and **contacts: ObservedArray\<Person>** in **AddressBookView**. The reasons are as follows: 1101 - The type of the \@Link decorated variable must be the same as that of the data source, and \@Link can only observe the changes at the first layer. 1102 - \@ObjectLink allows for initialization from the property of the data source. It functions as a proxy for the properties of the \@Observed decorated class and can observe the changes of the properties of that class. 1103 2. When the contact name (**Person.name**) or preferred phone number (**Person.phones[0]**) is updated, **PersonView** needs to be updated. As the update to **Person.phones[0]** occurs at the second layer, it cannot be observed if \@Link is used. In addition, \@Link requires its decorated variable be of the same type as the data source. Therefore, \@ObjectLink is required in **PersonView**, that is, \@ObjectLink **person: Person** and \@ObjectLink **phones: ObservedArray\<string>**. 1104 1105  1106 1107 Now you have a basic idea of how to build a view model. In the root node of an application, the view model may comprise a huge amount of nested data, which is more often the case. Yet, you can make reasonable separation of the data in the UI tree structure. You can adapt the view model data items to views so that the view at each layer contains relatively flat data, and you only need to observe changes at the current layer. 1108 1109 In this way, the UI re-render workload is minimized, leading to higher application performance. 1110 1111 The complete sample code is as follows: 1112 1113 1114```ts 1115// ViewModel classes 1116let nextId = 0; 1117 1118@Observed 1119export class ObservedArray<T> extends Array<T> { 1120 constructor(args: T[]) { 1121 console.log(`ObservedArray: ${JSON.stringify(args)} `) 1122 if (args instanceof Array) { 1123 super(...args); 1124 } else { 1125 super(args) 1126 } 1127 } 1128} 1129 1130@Observed 1131export class Address { 1132 street: string; 1133 zip: number; 1134 city: string; 1135 1136 constructor(street: string, 1137 zip: number, 1138 city: string) { 1139 this.street = street; 1140 this.zip = zip; 1141 this.city = city; 1142 } 1143} 1144 1145@Observed 1146export class Person { 1147 id_: string; 1148 name: string; 1149 address: Address; 1150 phones: ObservedArray<string>; 1151 1152 constructor(name: string, 1153 street: string, 1154 zip: number, 1155 city: string, 1156 phones: string[]) { 1157 this.id_ = `${nextId}`; 1158 nextId++; 1159 this.name = name; 1160 this.address = new Address(street, zip, city); 1161 this.phones = new ObservedArray<string>(phones); 1162 } 1163} 1164 1165export class AddressBook { 1166 me: Person; 1167 contacts: ObservedArray<Person>; 1168 1169 constructor(me: Person, contacts: Person[]) { 1170 this.me = me; 1171 this.contacts = new ObservedArray<Person>(contacts); 1172 } 1173} 1174 1175// Render the name of the Person object and the first phone number in the @Observed array <string>. 1176// To update the phone number, @ObjectLink person and @ObjectLink phones are required. 1177// this.person.phones cannot be used. Otherwise, changes to items inside the array will not be observed. 1178// Update selectedPerson in onClick in AddressBookView and PersonEditView. 1179@Component 1180struct PersonView { 1181 @ObjectLink person: Person; 1182 @ObjectLink phones: ObservedArray<string>; 1183 @Link selectedPerson: Person; 1184 1185 build() { 1186 Flex({ direction: FlexDirection.Row, justifyContent: FlexAlign.SpaceBetween }) { 1187 Text(this.person.name) 1188 if (this.phones.length) { 1189 Text(this.phones[0]) 1190 } 1191 } 1192 .height(55) 1193 .backgroundColor(this.selectedPerson.name == this.person.name ? "#ffa0a0" : "#ffffff") 1194 .onClick(() => { 1195 this.selectedPerson = this.person; 1196 }) 1197 } 1198} 1199 1200@Component 1201struct phonesNumber { 1202 @ObjectLink phoneNumber: ObservedArray<string> 1203 1204 build() { 1205 Column() { 1206 1207 ForEach(this.phoneNumber, 1208 (phone: ResourceStr, index?: number) => { 1209 TextInput({ text: phone }) 1210 .width(150) 1211 .onChange((value) => { 1212 console.log(`${index}. ${value} value has changed`) 1213 this.phoneNumber[index!] = value; 1214 }) 1215 }, 1216 (phone: ResourceStr, index: number) => `${this.phoneNumber[index] + index}` 1217 ) 1218 } 1219 } 1220} 1221 1222 1223// Render the information about the contact (person). 1224// The @Prop decorated variable makes a deep copy from the parent component AddressBookView and retains the changes locally. The changes of TextInput apply only to the local copy. 1225// Click Save Changes to copy all data to @Link through @Prop and synchronize the data to other components. 1226@Component 1227struct PersonEditView { 1228 @Consume addrBook: AddressBook; 1229 /* Reference pointing to selectedPerson in the parent component. */ 1230 @Link selectedPerson: Person; 1231 /* Make changes on the local copy until you click Save Changes. */ 1232 @Prop name: string = ""; 1233 @Prop address: Address = new Address("", 0, ""); 1234 @Prop phones: ObservedArray<string> = []; 1235 1236 selectedPersonIndex(): number { 1237 return this.addrBook.contacts.findIndex((person: Person) => person.id_ == this.selectedPerson.id_); 1238 } 1239 1240 build() { 1241 Column() { 1242 TextInput({ text: this.name }) 1243 .onChange((value) => { 1244 this.name = value; 1245 }) 1246 TextInput({ text: this.address.street }) 1247 .onChange((value) => { 1248 this.address.street = value; 1249 }) 1250 1251 TextInput({ text: this.address.city }) 1252 .onChange((value) => { 1253 this.address.city = value; 1254 }) 1255 1256 TextInput({ text: this.address.zip.toString() }) 1257 .onChange((value) => { 1258 const result = Number.parseInt(value); 1259 this.address.zip = Number.isNaN(result) ? 0 : result; 1260 }) 1261 1262 if (this.phones.length > 0) { 1263 phonesNumber({ phoneNumber: this.phones }) 1264 } 1265 1266 Flex({ direction: FlexDirection.Row, justifyContent: FlexAlign.SpaceBetween }) { 1267 Text("Save Changes") 1268 .onClick(() => { 1269 // Assign the updated value of the local copy to the reference pointing to selectedPerson in the parent component. 1270 // Do not create new objects. Modify the properties of the existing objects instead. 1271 this.selectedPerson.name = this.name; 1272 this.selectedPerson.address = new Address(this.address.street, this.address.zip, this.address.city) 1273 this.phones.forEach((phone: string, index: number) => { 1274 this.selectedPerson.phones[index] = phone 1275 }); 1276 }) 1277 if (this.selectedPersonIndex() != -1) { 1278 Text("Delete Contact") 1279 .onClick(() => { 1280 let index = this.selectedPersonIndex(); 1281 console.log(`delete contact at index ${index}`); 1282 1283 // Delete the current contact. 1284 this.addrBook.contacts.splice(index, 1); 1285 1286 // Delete the current selectedPerson. The selected contact is then changed to the contact immediately before the deleted contact. 1287 index = (index < this.addrBook.contacts.length) ? index : index - 1; 1288 1289 // If all contracts are deleted, the me object is selected. 1290 this.selectedPerson = (index >= 0) ? this.addrBook.contacts[index] : this.addrBook.me; 1291 }) 1292 } 1293 } 1294 1295 } 1296 } 1297} 1298 1299@Component 1300struct AddressBookView { 1301 @ObjectLink me: Person; 1302 @ObjectLink contacts: ObservedArray<Person>; 1303 @State selectedPerson: Person = new Person("", "", 0, "", []); 1304 1305 aboutToAppear() { 1306 this.selectedPerson = this.me; 1307 } 1308 1309 build() { 1310 Flex({ direction: FlexDirection.Column, justifyContent: FlexAlign.Start }) { 1311 Text("Me:") 1312 PersonView({ 1313 person: this.me, 1314 phones: this.me.phones, 1315 selectedPerson: this.selectedPerson 1316 }) 1317 1318 Divider().height(8) 1319 1320 ForEach(this.contacts, (contact: Person) => { 1321 PersonView({ 1322 person: contact, 1323 phones: contact.phones as ObservedArray<string>, 1324 selectedPerson: this.selectedPerson 1325 }) 1326 }, 1327 (contact: Person): string => { 1328 return contact.id_; 1329 } 1330 ) 1331 1332 Divider().height(8) 1333 1334 Text("Edit:") 1335 PersonEditView({ 1336 selectedPerson: this.selectedPerson, 1337 name: this.selectedPerson.name, 1338 address: this.selectedPerson.address, 1339 phones: this.selectedPerson.phones 1340 }) 1341 } 1342 .borderStyle(BorderStyle.Solid).borderWidth(5).borderColor(0xAFEEEE).borderRadius(5) 1343 } 1344} 1345 1346@Entry 1347@Component 1348struct PageEntry { 1349 @Provide addrBook: AddressBook = new AddressBook( 1350 new Person("Gigi", "Itamerenkatu 9", 180, "Helsinki", ["18*********", "18*********", "18*********"]), 1351 [ 1352 new Person("Oly", "Itamerenkatu 9", 180, "Helsinki", ["11*********", "12*********"]), 1353 new Person("Sam", "Itamerenkatu 9", 180, "Helsinki", ["13*********", "14*********"]), 1354 new Person("Vivi", "Itamerenkatu 9", 180, "Helsinki", ["15*********", "168*********"]), 1355 ]); 1356 1357 build() { 1358 Column() { 1359 AddressBookView({ 1360 me: this.addrBook.me, 1361 contacts: this.addrBook.contacts, 1362 selectedPerson: this.addrBook.me 1363 }) 1364 } 1365 } 1366} 1367``` 1368 1369