• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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![en-us_image_0000001653986573](figures/en-us_image_0000001653986573.png)
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   ![en-us_image_0000001638250945](figures/en-us_image_0000001638250945.png)
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![en-us_image_0000001588450934](figures/en-us_image_0000001588450934.png)
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 \@Component decorator 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 \@Component decorators: one for rendering an external array/object, and the other for rendering a class object nested within the array/object. Variables decorated by \@Prop, \@Link, and \@ObjectLink can only observe changes at the first layer.
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![en-us_image_0000001588610894](figures/en-us_image_0000001588610894.png)
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&lt;ClassA&gt, 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:
765
766- \@ObjectLink decorated variables are readable only and cannot be assigned values, whereas \@Prop decorated variables can be assigned values.
767
768- \@ObjectLink implements two-way data synchronization because it is initialized through a reference to the data source.
769
770- \@Prop implements one-way data synchronization and requires a deep copy of the data source.
771
772- 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.
773
774
775## Example
776
777
778The 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.
779
780
781Let's develop a phonebook application to implement the following features:
782
783
784- Display the phone numbers of contacts and the local device ("Me").
785
786- You can select a contact and edit its information, including the phone number and address.
787
788- When you update contact information, the changes are saved only after you click **Save Changes**.
789
790- You can click **Delete Contact** to delete a contact from the contacts list.
791
792
793In this example, the view model needs to include the following:
794
795
796- **AddressBook** (class)
797  - **me**: stores a **Person** class.
798  - **contacts**: stores a **Person** class array.
799
800
801The **AddressBook** class is declared as follows:
802
803
804
805```ts
806export class AddressBook {
807  me: Person;
808  contacts: ObservedArray<Person>;
809
810  constructor(me: Person, contacts: Person[]) {
811    this.me = me;
812    this.contacts = new ObservedArray<Person>(contacts);
813  }
814}
815```
816
817
818- Person (class)
819  - name : string
820  - address: Address
821  - phones: ObservedArray&lt;string&gt;
822  - Address (class)
823    - street: string
824    - zip: number
825    - city: string
826
827
828The **Address** class is declared as follows:
829
830
831
832```ts
833@Observed
834export class Address {
835  street: string;
836  zip: number;
837  city: string;
838
839  constructor(street: string,
840              zip: number,
841              city: string) {
842    this.street = street;
843    this.zip = zip;
844    this.city = city;
845  }
846}
847```
848
849
850The **Person** class is declared as follows:
851
852
853
854```ts
855let nextId = 0;
856
857@Observed
858export class Person {
859  id_: string;
860  name: string;
861  address: Address;
862  phones: ObservedArray<string>;
863
864  constructor(name: string,
865              street: string,
866              zip: number,
867              city: string,
868              phones: string[]) {
869    this.id_ = `${nextId}`;
870    nextId++;
871    this.name = name;
872    this.address = new Address(street, zip, city);
873    this.phones = new ObservedArray<string>(phones);
874  }
875}
876```
877
878
879Note 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:
880
881
882
883```ts
884@Observed
885export class ObservedArray<T> extends Array<T> {
886  constructor(args: T[]) {
887    console.log(`ObservedArray: ${JSON.stringify(args)} `)
888    if (args instanceof Array) {
889      super(...args);
890    } else {
891      super(args)
892    }
893  }
894}
895```
896
897
898- **selected**: reference to **Person**.
899
900
901The update process is as follows:
902
903
9041. 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.
905   **PageEntry** and **AddressBookView** are declared as follows:
906
907
908   ```ts
909   @Component
910   struct AddressBookView {
911
912       @ObjectLink me : Person;
913       @ObjectLink contacts : ObservedArray<Person>;
914       @State selectedPerson: Person = new Person("", "", 0, "", []);
915
916       aboutToAppear() {
917           this.selectedPerson = this.me;
918       }
919
920       build() {
921           Flex({ direction: FlexDirection.Column, justifyContent: FlexAlign.Start}) {
922               Text("Me:")
923               PersonView({
924                person: this.me,
925                phones: this.me.phones,
926                selectedPerson: this.selectedPerson
927              })
928
929               Divider().height(8)
930
931              ForEach(this.contacts, (contact: Person) => {
932                PersonView({
933                  person: contact,
934                  phones: contact.phones as ObservedArray<string>,
935                  selectedPerson: this.selectedPerson
936                })
937              },
938                (contact: Person): string => { return contact.id_; }
939              )
940
941               Divider().height(8)
942
943               Text("Edit:")
944               PersonEditView({
945                selectedPerson: this.selectedPerson,
946                name: this.selectedPerson.name,
947                address: this.selectedPerson.address,
948                phones: this.selectedPerson.phones
949              })
950           }
951               .borderStyle(BorderStyle.Solid).borderWidth(5).borderColor(0xAFEEEE).borderRadius(5)
952       }
953   }
954
955   @Entry
956   @Component
957   struct PageEntry {
958     @Provide addrBook: AddressBook = new AddressBook(
959       new Person("Gigi", "Itamerenkatu 9", 180, "Helsinki", ["18*********", "18*********", "18*********"]),
960       [
961         new Person("Oly", "Itamerenkatu 9", 180, "Helsinki", ["18*********", "18*********"]),
962         new Person("Sam", "Itamerenkatu 9", 180, "Helsinki", ["18*********", "18*********"]),
963         new Person("Vivi", "Itamerenkatu 9", 180, "Helsinki", ["18*********", "18*********"]),
964       ]);
965
966     build() {
967       Column() {
968         AddressBookView({
969          me: this.addrBook.me,
970          contacts: this.addrBook.contacts,
971          selectedPerson: this.addrBook.me
972        })
973       }
974     }
975   }
976   ```
977
9782. **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.
979   **PersonView** is declared as follows:
980
981
982   ```ts
983   // Display the contact name and preferred phone number.
984   // To update the phone number, @ObjectLink person and @ObjectLink phones are required.
985   // 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.
986   // Trigger the onClick event to update selectedPerson.
987   @Component
988   struct PersonView {
989
990       @ObjectLink person : Person;
991       @ObjectLink phones :  ObservedArray<string>;
992
993       @Link selectedPerson : Person;
994
995       build() {
996           Flex({ direction: FlexDirection.Row, justifyContent: FlexAlign.SpaceBetween }) {
997             Text(this.person.name)
998             if (this.phones.length > 0) {
999               Text(this.phones[0])
1000             }
1001           }
1002           .height(55)
1003           .backgroundColor(this.selectedPerson.name == this.person.name ? "#ffa0a0" : "#ffffff")
1004           .onClick(() => {
1005               this.selectedPerson = this.person;
1006           })
1007       }
1008   }
1009   ```
1010
10113. The information about the selected contact (person) is displayed in the **PersonEditView** object. The data synchronization for the **PersonEditView** can be implemented as follows:
1012
1013   - 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).
1014
1015   - 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.
1016
1017   - 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:
1018
1019     ```ts
1020     // Render the information about the contact (person).
1021     // 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.
1022     // Click Save Changes to copy all data to @Link through @Prop and synchronize the data to other components.
1023     @Component
1024     struct PersonEditView {
1025
1026         @Consume addrBook : AddressBook;
1027
1028         /* Reference pointing to selectedPerson in the parent component. */
1029         @Link selectedPerson: Person;
1030
1031         /* Make changes on the local copy until you click Save Changes. */
1032         @Prop name: string = "";
1033         @Prop address : Address = new Address("", 0, "");
1034         @Prop phones : ObservedArray<string> = [];
1035
1036         selectedPersonIndex() : number {
1037             return this.addrBook.contacts.findIndex((person: Person) => person.id_ == this.selectedPerson.id_);
1038         }
1039
1040         build() {
1041             Column() {
1042                 TextInput({ text: this.name})
1043                     .onChange((value) => {
1044                         this.name = value;
1045                       })
1046                 TextInput({text: this.address.street})
1047                     .onChange((value) => {
1048                         this.address.street = value;
1049                     })
1050
1051                 TextInput({text: this.address.city})
1052                     .onChange((value) => {
1053                         this.address.city = value;
1054                     })
1055
1056                 TextInput({text: this.address.zip.toString()})
1057                     .onChange((value) => {
1058                         const result = Number.parseInt(value);
1059                         this.address.zip= Number.isNaN(result) ? 0 : result;
1060                     })
1061
1062                 if (this.phones.length > 0) {
1063                   ForEach(this.phones,
1064                     (phone: ResourceStr, index?:number) => {
1065                       TextInput({ text: phone })
1066                         .width(150)
1067                         .onChange((value) => {
1068                           console.log(`${index}. ${value} value has changed`)
1069                           this.phones[index!] = value;
1070                         })
1071                     },
1072                     (phone: ResourceStr, index?:number) => `${index}`
1073                   )
1074                 }
1075
1076                 Flex({ direction: FlexDirection.Row, justifyContent: FlexAlign.SpaceBetween }) {
1077                     Text("Save Changes")
1078                         .onClick(() => {
1079                             // Assign the updated value of the local copy to the reference pointing to selectedPerson in the parent component.
1080                             // Do not create new objects. Modify the properties of the existing objects instead.
1081                             this.selectedPerson.name = this.name;
1082                             this.selectedPerson.address = new Address(this.address.street, this.address.zip, this.address.city)
1083                             this.phones.forEach((phone : string, index : number) => { this.selectedPerson.phones[index] = phone } );
1084                         })
1085                     if (this.selectedPersonIndex()!=-1) {
1086                         Text("Delete Contact")
1087                             .onClick(() => {
1088                                 let index = this.selectedPersonIndex();
1089                                 console.log(`delete contact at index ${index}`);
1090
1091                                 // Delete the current contact.
1092                                 this.addrBook.contacts.splice(index, 1);
1093
1094                                 // Delete the current selectedPerson. The selected contact is then changed to the contact immediately before the deleted contact.
1095                                 index = (index < this.addrBook.contacts.length) ? index : index-1;
1096
1097                                 // If all contracts are deleted, the me object is selected.
1098                                 this.selectedPerson = (index>=0) ? this.addrBook.contacts[index] : this.addrBook.me;
1099                             })
1100                     }
1101                 }
1102
1103             }
1104         }
1105     }
1106     ```
1107
1108     Pay attention to the following differences between \@ObjectLink and \@Link.
1109
1110     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:
1111     - 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.
1112
1113     - \@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.
1114- 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>**.
1115
1116![en-us_image_0000001605293914](figures/en-us_image_0000001605293914.png)
1117
1118Now 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.
1119
1120In this way, the UI re-render workload is minimized, leading to higher application performance.
1121
1122     The complete sample code is as follows:
1123
1124
1125```ts
1126
1127// ViewModel classes
1128let nextId = 0;
1129
1130@Observed
1131export class ObservedArray<T> extends Array<T> {
1132  constructor(args: T[]) {
1133    console.log(`ObservedArray: ${JSON.stringify(args)} `)
1134    if (args instanceof Array) {
1135      super(...args);
1136    } else {
1137      super(args)
1138    }
1139  }
1140}
1141
1142@Observed
1143export class Address {
1144  street: string;
1145  zip: number;
1146  city: string;
1147
1148  constructor(street: string,
1149              zip: number,
1150              city: string) {
1151    this.street = street;
1152    this.zip = zip;
1153    this.city = city;
1154  }
1155}
1156
1157@Observed
1158export class Person {
1159  id_: string;
1160  name: string;
1161  address: Address;
1162  phones: ObservedArray<string>;
1163
1164  constructor(name: string,
1165              street: string,
1166              zip: number,
1167              city: string,
1168              phones: string[]) {
1169    this.id_ = `${nextId}`;
1170    nextId++;
1171    this.name = name;
1172    this.address = new Address(street, zip, city);
1173    this.phones = new ObservedArray<string>(phones);
1174  }
1175}
1176
1177export class AddressBook {
1178  me: Person;
1179  contacts: ObservedArray<Person>;
1180
1181  constructor(me: Person, contacts: Person[]) {
1182    this.me = me;
1183    this.contacts = new ObservedArray<Person>(contacts);
1184  }
1185}
1186
1187// Render the name of the Person object and the first phone number in the @Observed array <string>.
1188// To update the phone number, @ObjectLink person and @ObjectLink phones are required.
1189// this.person.phones cannot be used. Otherwise, changes to items inside the array will not be observed.
1190// Update selectedPerson in onClick in AddressBookView and PersonEditView.
1191@Component
1192struct PersonView {
1193  @ObjectLink person: Person;
1194  @ObjectLink phones: ObservedArray<string>;
1195  @Link selectedPerson: Person;
1196
1197  build() {
1198    Flex({ direction: FlexDirection.Row, justifyContent: FlexAlign.SpaceBetween }) {
1199      Text(this.person.name)
1200      if (this.phones.length) {
1201        Text(this.phones[0])
1202      }
1203    }
1204    .height(55)
1205    .backgroundColor(this.selectedPerson.name == this.person.name ? "#ffa0a0" : "#ffffff")
1206    .onClick(() => {
1207      this.selectedPerson = this.person;
1208    })
1209  }
1210}
1211
1212// Render the information about the contact (person).
1213// 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.
1214// Click Save Changes to copy all data to @Link through @Prop and synchronize the data to other components.
1215@Component
1216struct PersonEditView {
1217  @Consume addrBook: AddressBook;
1218
1219  /* Reference pointing to selectedPerson in the parent component. */
1220  @Link selectedPerson: Person;
1221
1222  /* Make changes on the local copy until you click Save Changes. */
1223  @Prop name: string = "";
1224  @Prop address: Address = new Address("", 0, "");
1225  @Prop phones: ObservedArray<string> = [];
1226
1227  selectedPersonIndex(): number {
1228    return this.addrBook.contacts.findIndex((person: Person) => person.id_ == this.selectedPerson.id_);
1229  }
1230
1231  build() {
1232    Column() {
1233      TextInput({ text: this.name })
1234        .onChange((value) => {
1235          this.name = value;
1236        })
1237      TextInput({ text: this.address.street })
1238        .onChange((value) => {
1239          this.address.street = value;
1240        })
1241
1242      TextInput({ text: this.address.city })
1243        .onChange((value) => {
1244          this.address.city = value;
1245        })
1246
1247      TextInput({ text: this.address.zip.toString() })
1248        .onChange((value) => {
1249          const result = Number.parseInt(value);
1250          this.address.zip = Number.isNaN(result) ? 0 : result;
1251        })
1252
1253      if (this.phones.length > 0) {
1254        ForEach(this.phones,
1255          (phone: ResourceStr, index?:number) => {
1256            TextInput({ text: phone })
1257              .width(150)
1258              .onChange((value) => {
1259                console.log(`${index}. ${value} value has changed`)
1260                this.phones[index!] = value;
1261              })
1262          },
1263          (phone: ResourceStr, index?:number) => `${index}`
1264        )
1265      }
1266
1267      Flex({ direction: FlexDirection.Row, justifyContent: FlexAlign.SpaceBetween }) {
1268        Text("Save Changes")
1269          .onClick(() => {
1270            // Assign the updated value of the local copy to the reference pointing to selectedPerson in the parent component.
1271            // Do not create new objects. Modify the properties of the existing objects instead.
1272            this.selectedPerson.name = this.name;
1273            this.selectedPerson.address = new Address(this.address.street, this.address.zip, this.address.city)
1274            this.phones.forEach((phone: string, index: number) => {
1275              this.selectedPerson.phones[index] = phone
1276            });
1277          })
1278        if (this.selectedPersonIndex() != -1) {
1279          Text("Delete Contact")
1280            .onClick(() => {
1281              let index = this.selectedPersonIndex();
1282              console.log(`delete contact at index ${index}`);
1283
1284              // Delete the current contact.
1285              this.addrBook.contacts.splice(index, 1);
1286
1287              // Delete the current selectedPerson. The selected contact is then changed to the contact immediately before the deleted contact.
1288              index = (index < this.addrBook.contacts.length) ? index : index - 1;
1289
1290              // If all contracts are deleted, the me object is selected.
1291              this.selectedPerson = (index >= 0) ? this.addrBook.contacts[index] : this.addrBook.me;
1292            })
1293        }
1294      }
1295
1296    }
1297  }
1298}
1299
1300@Component
1301struct AddressBookView {
1302  @ObjectLink me: Person;
1303  @ObjectLink contacts: ObservedArray<Person>;
1304  @State selectedPerson: Person = new Person("", "", 0, "", []);
1305
1306  aboutToAppear() {
1307    this.selectedPerson = this.me;
1308  }
1309
1310  build() {
1311    Flex({ direction: FlexDirection.Column, justifyContent: FlexAlign.Start }) {
1312      Text("Me:")
1313      PersonView({
1314        person: this.me,
1315        phones: this.me.phones,
1316        selectedPerson: this.selectedPerson
1317      })
1318
1319      Divider().height(8)
1320
1321      ForEach(this.contacts, (contact: Person) => {
1322        PersonView({
1323          person: contact,
1324          phones: contact.phones as ObservedArray<string>,
1325          selectedPerson: this.selectedPerson
1326        })
1327      },
1328        (contact: Person): string => { return contact.id_; }
1329      )
1330
1331      Divider().height(8)
1332
1333      Text("Edit:")
1334      PersonEditView({
1335        selectedPerson: this.selectedPerson,
1336        name: this.selectedPerson.name,
1337        address: this.selectedPerson.address,
1338        phones: this.selectedPerson.phones
1339      })
1340    }
1341    .borderStyle(BorderStyle.Solid).borderWidth(5).borderColor(0xAFEEEE).borderRadius(5)
1342  }
1343}
1344
1345@Entry
1346@Component
1347struct PageEntry {
1348  @Provide addrBook: AddressBook = new AddressBook(
1349    new Person("Gigi", "Itamerenkatu 9", 180, "Helsinki", ["18*********", "18*********", "18*********"]),
1350    [
1351      new Person("Oly", "Itamerenkatu 9", 180, "Helsinki", ["18*********", "18*********"]),
1352      new Person("Sam", "Itamerenkatu 9", 180, "Helsinki", ["18*********", "18*********"]),
1353      new Person("Vivi", "Itamerenkatu 9", 180, "Helsinki", ["18*********", "18*********"]),
1354    ]);
1355
1356  build() {
1357    Column() {
1358      AddressBookView({
1359        me: this.addrBook.me,
1360        contacts: this.addrBook.contacts,
1361        selectedPerson: this.addrBook.me
1362      })
1363    }
1364  }
1365}
1366```
1367