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