1# MVVM 2 3After understanding the basic concepts of state management, you may be eager to develop your own applications. However, if the project structure is not carefully planned at the early stage of application development, the relationship between components becomes blurred as the project grows and state variables increase. In this case, the development of any new function may cause a chain reaction and increase the maintenance cost. This topic describes the MVVM mode and the relationship between the UI development mode of ArkUI and MVVM, and provides guidance for you to design your own project structures to facilitate product development and maintenance during product iteration and upgrade. 4 5 6Most decorators are covered in this topic, therefore, you are advised to read [State Management Overview](./arkts-state-management-overview.md) and topics related to decorators of V1 to have a basic understanding of state management V1 before getting started. 7 8## Introduction 9 10### Concepts 11 12During application development, UI updates need to be synchronized in real time with data state changes. This synchronization usually determines the performance and user experience of applications. To reduce the complexity of data and UI synchronization, ArkUI uses the Model-View-ViewModel (MVVM) architecture. The MVVM divides an application into three core parts: Model, View, and ViewModel to separate data, views, and logic. In this mode, the UI can be automatically updated with the state change without manual processing, thereby more efficiently managing the binding and update of data and views. 13 14- Model: stores and manages application data and service logic without directly interacting with the UI. Generally, Model obtains data from back-end APIs and serves as the data basis of applications, which ensures data consistency and integrity. 15- View: displays data on the UI and interacts with users. No service logic is contained. It dynamically updates the UI by binding the data provided by the ViewModel. 16- ViewModel: manages UI state and interaction logic. As a bridge between Model and View, a View usually corresponds to a ViewModel. The ViewModel listens for data changes in Model, notifies View to update the UI, processes user interaction events, and converts the events into data operations. 17 18The UI development mode of ArkUI belongs to the MVVM mode. By introducing the concept of MVVM, you may have basic understanding on how the state management work in MVVM. State management aims to drive data update and enable you to focus only on page design without paying attention to the UI re-render logic. In addition, ViewModel enables state variables to automatically maintain data. In this way, MVVM provides a more efficient way for you to develop applications. 19 20### ArkUI Development 21 22The UI development mode of ArkUI is the MVVM mode, in which the state variables play the role of ViewModel to re-render the UI and data. The following figure shows the overall architecture. 23 24 25 26### Layer Description 27 28**View** 29 30* Page components: All applications are classified by page, such as the login page, list page, editing page, help page, and copyright page. The data required by each page may be completely different, or the same set of data can be shared with multiple pages. 31* Business components: a functional component that has some service capabilities of the application. Typically, the business component may be associated with the data in the ViewModel of the project and cannot be shared with other projects. 32* Common components: Similar to built-in components, these components are not associated with the ViewModel data in the application. These components can be shared across multiple projects to implement common functions. 33 34**ViewModel** 35 36* Page data: Data that is organized by page. When a user browses a page, some pages may not be displayed. Therefore, it is recommended that the page data be designed using lazy import. 37 38> The differences between the ViewModel data and the Model data are as follows: 39> 40> Model data, a set of service data of the application, is organized based on the entire project. 41> 42> ViewModel data provides data used on a page. It may be a part of the service data of the entire application. In addition, ViewModel also provides auxiliary data for page display, which may be irrelevant to the application services. 43 44**Model** 45 46The Model layer is the original data provider of applications. 47 48### Core Principles of the Architecture 49 50**Cross-layer access is not allowed.** 51 52* View cannot directly call data from Model. Instead, use the methods provided by ViewModel to call. 53* Model data cannot modify the UI directly but notifies the ViewModel to update the data. 54 55**The lower layer cannot access the upper layer data.** 56 57The lower layer can only notify the upper layer to update the data. In the service logic, you cannot write code at the lower layer to obtain the upper-layer data. For example, the logic processing at ViewModel cannot depend on a value on the UI at View. 58 59**Non-parent-child components cannot directly access each other.** 60 61This is the core principle of View design. A component should comply with the following logic: 62 63* Do not directly access the parent component. Event or subscription capability must be used. 64* Do not directly access sibling components. This is because components can access only the child nodes (through parameter passing) and parent nodes (through events or notifications) that they can see. In this way, components are decoupled. 65 66Reasons: 67 68* The child components used by the component are clear, therefore, access is allowed. 69* The parent node where the component is placed is unknown. Therefore, the component can access the parent node only through notifications or events. 70* It is impossible for a component to know its sibling nodes, so the component cannot manipulate the sibling nodes. 71 72## Memo Development 73 74This section describes how to use ArkUI to design your own applications. The sample code in this section directly develops functions without designing the code architecture and considering subsequent maintenance, and the decorators required for function development are introduced as well. 75 76### @State 77 78* As the most commonly used decorator, @State is used to define state variables. Generally, the @State decorator is used as the data source of the parent component. When you click @State, the state variable is updated to re-render the UI. If the @State decorator is removed, the UI cannot be refreshed. 79 80```typescript 81@Entry 82@Component 83struct Index { 84 @State isFinished: boolean = false; 85 86 build() { 87 Column() { 88 Row() { 89 Text('To-Dos') 90 .fontSize(30) 91 .fontWeight(FontWeight.Bold) 92 } 93 .width('100%') 94 .margin({top: 10, bottom: 10}) 95 96 // To-Do list 97 Row({space: 15}) { 98 if (this.isFinished) { 99 // 'app.media.finished' is only an example. Replace it with the actual one in use. Otherwise, the imageSource instance fails to be created, and subsequent operations cannot be performed. 100 Image($r('app.media.finished')) 101 .width(28) 102 .height(28) 103 } 104 else { 105 // 'app.media.unfinished' is only an example. Replace it with the actual one in use. Otherwise, the imageSource instance fails to be created, and subsequent operations cannot be performed. 106 Image($r('app.media.unfinished')) 107 .width(28) 108 .height(28) 109 } 110 Text('Learn maths') 111 .fontSize(24) 112 .fontWeight(450) 113 .decoration({type: this.isFinished ? TextDecorationType.LineThrough : TextDecorationType.None}) 114 } 115 .height('40%') 116 .width('100%') 117 .border({width: 5}) 118 .padding({left: 15}) 119 .onClick(() => { 120 this.isFinished = !this.isFinished; 121 }) 122 } 123 .height('100%') 124 .width('100%') 125 .margin({top: 5, bottom: 5}) 126 .backgroundColor('#90f1f3f5') 127 } 128} 129``` 130 131The following figure shows the final effect. 132 133 134 135### @Prop and @Link 136 137In the preceding example, all code is written in the @Entry decorated component. As more and more components need to be rendered, you need to split the @Entry decorated component and use the @Prop and @Link decorators to decorate the split child components. 138 139* @Prop creates a one-way synchronization between the parent and child components. The child component can perform deep copy of the data from the parent component or update the data from the parent component or itself. However, it cannot synchronize data from the parent component. 140* @Link creates a two-way synchronization between the parent and child components. When the parent component changes, all @Links are notified. In addition, when @Link is updated, the corresponding variables of the parent component are notified as well. 141 142```typescript 143@Component 144struct TodoComponent { 145 build() { 146 Row() { 147 Text('To-Dos') 148 .fontSize(30) 149 .fontWeight(FontWeight.Bold) 150 } 151 .width('100%') 152 .margin({top: 10, bottom: 10}) 153 } 154} 155 156@Component 157struct AllChooseComponent { 158 @Link isFinished: boolean; 159 160 build() { 161 Row() { 162 Button('Select All', {type: ButtonType.Normal}) 163 .onClick(() => { 164 this.isFinished = !this.isFinished; 165 }) 166 .fontSize(30) 167 .fontWeight(FontWeight.Bold) 168 .backgroundColor('#f7f6cc74') 169 } 170 .padding({left: 15}) 171 .width('100%') 172 .margin({top: 10, bottom: 10}) 173 } 174} 175 176@Component 177struct ThingsComponent1 { 178 @Prop isFinished: boolean; 179 180 build() { 181 // Task 1 182 Row({space: 15}) { 183 if (this.isFinished) { 184 // 'app.media.finished' is only an example. Replace it with the actual one in use. Otherwise, the imageSource instance fails to be created, and subsequent operations cannot be performed. 185 Image($r('app.media.finished')) 186 .width(28) 187 .height(28) 188 } 189 else { 190 // 'app.media.unfinished' is only an example. Replace it with the actual one in use. Otherwise, the imageSource instance fails to be created, and subsequent operations cannot be performed. 191 Image($r('app.media.unfinished')) 192 .width(28) 193 .height(28) 194 } 195 Text('Study language') 196 .fontSize(24) 197 .fontWeight(450) 198 .decoration({type: this.isFinished ? TextDecorationType.LineThrough : TextDecorationType.None}) 199 } 200 .height('40%') 201 .width('100%') 202 .border({width: 5}) 203 .padding({left: 15}) 204 .onClick(() => { 205 this.isFinished = !this.isFinished; 206 }) 207 } 208} 209 210@Component 211struct ThingsComponent2 { 212 @Prop isFinished: boolean; 213 214 build() { 215 // Task 1 216 Row({space: 15}) { 217 if (this.isFinished) { 218 // 'app.media.finished' is only an example. Replace it with the actual one in use. Otherwise, the imageSource instance fails to be created, and subsequent operations cannot be performed. 219 Image($r('app.media.finished')) 220 .width(28) 221 .height(28) 222 } 223 else { 224 // 'app.media.unfinished' is only an example. Replace it with the actual one in use. Otherwise, the imageSource instance fails to be created, and subsequent operations cannot be performed. 225 Image($r('app.media.unfinished')) 226 .width(28) 227 .height(28) 228 } 229 Text('Learn maths') 230 .fontSize(24) 231 .fontWeight(450) 232 .decoration({type: this.isFinished ? TextDecorationType.LineThrough : TextDecorationType.None}) 233 } 234 .height('40%') 235 .width('100%') 236 .border({width: 5}) 237 .padding({left: 15}) 238 .onClick(() => { 239 this.isFinished = !this.isFinished; 240 }) 241 } 242} 243 244@Entry 245@Component 246struct Index { 247 @State isFinished: boolean = false; 248 249 build() { 250 Column() { 251 // All To-Do items. 252 TodoComponent() 253 254 // Select all. 255 AllChooseComponent({isFinished: this.isFinished}) 256 257 // Task 1 258 ThingsComponent1({isFinished: this.isFinished}) 259 260 // Task 2 261 ThingsComponent2({isFinished: this.isFinished}) 262 } 263 .height('100%') 264 .width('100%') 265 .margin({top: 5, bottom: 5}) 266 .backgroundColor('#90f1f3f5') 267 } 268} 269``` 270 271The following figure shows the effect. 272 273 274 275### Rendering Repeated Components 276 277* In the previous example, although the child component is split, the code of component 1 is similar to that of component 2. When the rendered components have the same configurations except data, **ForEach** is used to render the repeated components. 278* In this way, redundant code is decreased and the code structure is clearer. 279 280```typescript 281@Component 282struct TodoComponent { 283 build() { 284 Row() { 285 Text('To-Dos') 286 .fontSize(30) 287 .fontWeight(FontWeight.Bold) 288 } 289 .width('100%') 290 .margin({top: 10, bottom: 10}) 291 } 292} 293 294@Component 295struct AllChooseComponent { 296 @Link isFinished: boolean; 297 298 build() { 299 Row() { 300 Button('Select All', {type: ButtonType.Normal}) 301 .onClick(() => { 302 this.isFinished = !this.isFinished; 303 }) 304 .fontSize(30) 305 .fontWeight(FontWeight.Bold) 306 .backgroundColor('#f7f6cc74') 307 } 308 .padding({left: 15}) 309 .width('100%') 310 .margin({top: 10, bottom: 10}) 311 } 312} 313 314@Component 315struct ThingsComponent { 316 @Prop isFinished: boolean; 317 @Prop things: string; 318 build() { 319 // Task 1 320 Row({space: 15}) { 321 if (this.isFinished) { 322 // 'app.media.finished' is only an example. Replace it with the actual one in use. Otherwise, the imageSource instance fails to be created, and subsequent operations cannot be performed. 323 Image($r('app.media.finished')) 324 .width(28) 325 .height(28) 326 } 327 else { 328 // 'app.media.unfinished' is only an example. Replace it with the actual one in use. Otherwise, the imageSource instance fails to be created, and subsequent operations cannot be performed. 329 Image($r('app.media.unfinished')) 330 .width(28) 331 .height(28) 332 } 333 Text(`${this.things}`) 334 .fontSize(24) 335 .fontWeight(450) 336 .decoration({type: this.isFinished ? TextDecorationType.LineThrough : TextDecorationType.None}) 337 } 338 .height('8%') 339 .width('90%') 340 .padding({left: 15}) 341 .opacity(this.isFinished ? 0.3: 1) 342 .border({width:1}) 343 .borderColor(Color.White) 344 .borderRadius(25) 345 .backgroundColor(Color.White) 346 .onClick(() => { 347 this.isFinished = !this.isFinished; 348 }) 349 } 350} 351 352@Entry 353@Component 354struct Index { 355 @State isFinished: boolean = false; 356 @State planList: string[] = [ 357 '7:30 Get up' 358 '8:30 Breakfast' 359 '11:30 Lunch' 360 '17:30 Dinner' 361 '21:30 Snack' 362 '22:30 Shower' 363 '1:30 Go to bed' 364 ]; 365 366 build() { 367 Column() { 368 // All To-Do items. 369 TodoComponent() 370 371 // Select all. 372 AllChooseComponent({isFinished: this.isFinished}) 373 374 List() { 375 ForEach(this.planList, (item: string) => { 376 // Task 1 377 ThingsComponent({isFinished: this.isFinished, things: item}) 378 .margin(5) 379 }) 380 } 381 382 } 383 .height('100%') 384 .width('100%') 385 .margin({top: 5, bottom: 5}) 386 .backgroundColor('#90f1f3f5') 387 } 388} 389``` 390 391The following figure shows the effect. 392 393 394 395### @Builder 396 397* The **Builder** method is used to define methods in a component so that the same code can be reused in the component. 398* In this example, the @Builder method is used for deduplication and moving out data so that the code is clearer and easier to read. Compared with the initial code, the @Entry decorated component is used only to process page construction logic and does not process a large amount of content irrelevant to page design. 399 400```typescript 401@Observed 402class TodoListData { 403 planList: string[] = [ 404 '7:30 Get up' 405 '8:30 Breakfast' 406 '11:30 Lunch' 407 '17:30 Dinner' 408 '21:30 Snack' 409 '22:30 Shower' 410 '1:30 Go to bed' 411 ]; 412} 413 414@Component 415struct TodoComponent { 416 build() { 417 Row() { 418 Text('To-Dos') 419 .fontSize(30) 420 .fontWeight(FontWeight.Bold) 421 } 422 .width('100%') 423 .margin({top: 10, bottom: 10}) 424 } 425} 426 427@Component 428struct AllChooseComponent { 429 @Link isFinished: boolean; 430 431 build() { 432 Row() { 433 Button('Select All', {type: ButtonType.Capsule}) 434 .onClick(() => { 435 this.isFinished = !this.isFinished; 436 }) 437 .fontSize(30) 438 .fontWeight(FontWeight.Bold) 439 .backgroundColor('#f7f6cc74') 440 } 441 .padding({left: 15}) 442 .width('100%') 443 .margin({top: 10, bottom: 10}) 444 } 445} 446 447@Component 448struct ThingsComponent { 449 @Prop isFinished: boolean; 450 @Prop things: string; 451 452 @Builder displayIcon(icon: Resource) { 453 Image(icon) 454 .width(28) 455 .height(28) 456 .onClick(() => { 457 this.isFinished = !this.isFinished; 458 }) 459 } 460 461 build() { 462 // Task 1 463 Row({space: 15}) { 464 if (this.isFinished) { 465 // 'app.media.finished' is only an example. Replace it with the actual one in use. Otherwise, the imageSource instance fails to be created, and subsequent operations cannot be performed. 466 this.displayIcon($r('app.media.finished')); 467 } 468 else { 469 // 'app.media.unfinished' is only an example. Replace it with the actual one in use. Otherwise, the imageSource instance fails to be created, and subsequent operations cannot be performed. 470 this.displayIcon($r('app.media.unfinished')); 471 } 472 Text(`${this.things}`) 473 .fontSize(24) 474 .fontWeight(450) 475 .decoration({type: this.isFinished ? TextDecorationType.LineThrough : TextDecorationType.None}) 476 .onClick(() => { 477 this.things += '!' 478 }) 479 } 480 .height('8%') 481 .width('90%') 482 .padding({left: 15}) 483 .opacity(this.isFinished ? 0.3: 1) 484 .border({width:1}) 485 .borderColor(Color.White) 486 .borderRadius(25) 487 .backgroundColor(Color.White) 488 } 489} 490 491@Entry 492@Component 493struct Index { 494 @State isFinished: boolean = false; 495 @State data: TodoListData = new TodoListData(); 496 497 build() { 498 Column() { 499 // All To-Do items. 500 TodoComponent() 501 502 // Select all. 503 AllChooseComponent({isFinished: this.isFinished}) 504 505 List() { 506 ForEach(this.data.planList, (item: string) => { 507 // Task 1 508 ThingsComponent({isFinished: this.isFinished, things: item}) 509 .margin(5) 510 }) 511 } 512 513 } 514 .height('100%') 515 .width('100%') 516 .margin({top: 5, bottom: 5}) 517 .backgroundColor('#90f1f3f5') 518 } 519} 520``` 521 522 The following figure shows the effect. 523 524 525 526### Summary 527 528* After the code structure is optimized step by step, you can see that the @Entry decorated component serves as the entry of the page and the **build** function only needs to combine the required components, which is similar to building blocks. A child component called by a page is similar to a block and waits to be called by a required page. A state variable is similar to an adhesive. When a UI re-render event is triggered, the state variable can automatically re-render the bound component to implement on-demand page refresh. 529* Although the existing architecture does not use the MVVM design concept, the core concept of MVVM shows that the UI development of ArkUI should use the MVVM mode. Pages and components are at the View layer, and pages are responsible for combining components. A state variable is used to drive the component re-render to refresh the page. The ViewModel data needs to have a source, which is from the Model layer. 530* The code functions in the example are simple. However, as the number of functions increases, the code of the main page increases. When more functions are added to the Memo application and other pages need to use the components of the main page, how to organize the project structure? The MVVM mode is the answer. 531 532## Developing a To-Do List Through MVVM 533 534The previous section describes how to organize code in non-MVVM mode. As the code of the main page becomes larger, a proper layering method should be adopted to make the project structure clear and prevent components from referencing each other. Therefore, the entire system will not be affected during subsequent maintenance. This section uses MVVM to reorganize the code in the previous section to introduce the core file organization of MVVM. 535 536### MVVM File Structure 537 538* src 539 * ets 540 * pages ------ Stores page components. 541 * views ------ Stores business components. 542 * shares ------ Stores common components. 543 * service ------ Data services. 544 * app.ts ------ Service entry. 545 * LoginViewModel ----- Login page. 546 * xxxModel ------ Other pages. 547 548### Layered Design 549 550**Model** 551 552* The Model layer stores the core data structure of the application. This layer is not closely related to UI development. You can encapsulate the data structure based on your service logic. 553 554**ViewModel** 555 556> **NOTE** 557> 558> The ViewModel layer not only stores data, but also provides data services and processing. Therefore, many frameworks use "service" to represent this layer. 559 560* The ViewModel layer is the data layer that serves views. Generally, it has two features: 561 1. Data is organized based on pages. 562 2. Data on each page is lazy loaded. 563 564**View** 565 566The View layer is organized as required. You need to distinguish the following three types of components at this layer: 567 568* Page components: provides the overall page layout, implements redirection between multiple pages, and processes foreground and background events. 569* Business components: referenced by a page to construct a page. 570* Shared components: shared by multiple projects. 571 572> The differences between shared components and business components are as follows: 573> 574> A business component contains ViewModel data. Without ViewModel, the component cannot be executed. 575> 576> A shared component does not contain ViewModel data and requires external data. It contains a custom component that can work as long as external parameters (without service parameters) are met. 577 578### Example 579 580The file structure is reconstructed based on the MVVM mode as follows: 581 582* src 583 * ets 584 * Model 585 * ThingsModel 586 * TodoListModel 587 * pages 588 * Index 589 * View 590 * AllChooseComponent 591 * ThingsComponent 592 * TodoComponent 593 * TodoListComponent 594 * ViewModel 595 * ThingsViewModel 596 * TodoListViewModel 597 * resources 598 * rawfile 599 * defaultTasks.json 600 601The code is as follows: 602 603* Index.ets 604 605 ```typescript 606 import { common } from '@kit.AbilityKit'; 607 // import ViewModel 608 import TodoListViewModel from '../ViewModel/TodoListViewModel'; 609 610 // import View 611 import { TodoComponent } from '../View/TodoComponent'; 612 import { AllChooseComponent } from '../View/AllChooseComponent'; 613 import { TodoListComponent } from '../View/TodoListComponent'; 614 615 @Entry 616 @Component 617 struct TodoList { 618 @State thingsTodo: TodoListViewModel = new TodoListViewModel(); 619 private context = getContext(this) as common.UIAbilityContext; 620 621 async aboutToAppear() { 622 await this.thingsTodo.loadTasks(this.context); 623 } 624 625 build() { 626 Column() { 627 Row({ space: 40 }) { 628 // All To-Do items. 629 TodoComponent() 630 // Select all. 631 AllChooseComponent({ thingsViewModel: this.thingsTodo }) 632 } 633 634 Column() { 635 TodoListComponent({ thingsViewModelArray: this.thingsTodo.things }) 636 } 637 } 638 .height('100%') 639 .width('100%') 640 .margin({ top: 5, bottom: 5 }) 641 .backgroundColor('#90f1f3f5') 642 } 643 } 644 ``` 645 646 * ThingsModel.ets 647 648 ```typescript 649 export default class ThingsModel { 650 thingsName: string = 'Todo'; 651 isFinish: boolean = false; 652 } 653 ``` 654 655 * TodoListModel.ets 656 657 ```typescript 658 import { common } from '@kit.AbilityKit'; 659 import util from '@ohos.util'; 660 import ThingsModel from './ThingsModel'; 661 662 export default class TodoListModel { 663 things: Array<ThingsModel> = []; 664 665 constructor(things: Array<ThingsModel>) { 666 this.things = things; 667 } 668 669 async loadTasks(context: common.UIAbilityContext) { 670 let getJson = await context.resourceManager.getRawFileContent('defaultTasks.json'); 671 let textDecoderOptions: util.TextDecoderOptions = { ignoreBOM: true }; 672 let textDecoder = util.TextDecoder.create('utf-8', textDecoderOptions); 673 let result = textDecoder.decodeToString(getJson, { stream: false }); 674 this.things = JSON.parse(result); 675 } 676 } 677 ``` 678 679 * AllChooseComponent.ets 680 681 ```typescript 682 import TodoListViewModel from "../ViewModel/TodoListViewModel"; 683 684 @Component 685 export struct AllChooseComponent { 686 @State titleName: string = 'Select All'; 687 @Link thingsViewModel: TodoListViewModel; 688 689 build() { 690 Row() { 691 Button(`${this.titleName}`, { type: ButtonType.Capsule }) 692 .onClick(() => { 693 this.thingsViewModel.chooseAll(); 694 this.titleName = this.thingsViewModel.isChoosen ? 'Select All' : 'Deselect All' 695 }) 696 .fontSize(30) 697 .fontWeight(FontWeight.Bold) 698 .backgroundColor('#f7f6cc74') 699 } 700 .padding({ left: this.thingsViewModel.isChoosen ? 15 : 0 }) 701 .width('100%') 702 .margin({ top: 10, bottom: 10 }) 703 } 704 } 705 ``` 706 707 * ThingsComponent.ets 708 709 ```typescript 710 import ThingsViewModel from "../ViewModel/ThingsViewModel"; 711 712 @Component 713 export struct ThingsComponent { 714 @Prop things: ThingsViewModel; 715 716 @Builder 717 displayIcon(icon: Resource) { 718 Image(icon) 719 .width(28) 720 .height(28) 721 .onClick(() => { 722 this.things.updateIsFinish(); 723 }) 724 } 725 726 build() { 727 // To-Do list 728 Row({ space: 15 }) { 729 if(this.things.isFinish) { 730 // 'app.media.finished' is only an example. Replace it with the actual one in use. Otherwise, the imageSource instance fails to be created, and subsequent operations cannot be performed. 731 this.displayIcon($r('app.media.finished')); 732 } else { 733 // 'app.media.unfinished' is only an example. Replace it with the actual one in use. Otherwise, the imageSource instance fails to be created, and subsequent operations cannot be performed. 734 this.displayIcon($r('app.media.unfinished')); 735 } 736 737 Text(`${this.things.thingsName}`) 738 .fontSize(24) 739 .fontWeight(450) 740 .decoration({ type: this.things.isFinish ? TextDecorationType.LineThrough: TextDecorationType.None }) 741 .onClick(() => { 742 this.things.addSuffixes(); 743 }) 744 } 745 .height('8%') 746 .width('90%') 747 .padding({ left: 15 }) 748 .opacity(this.things.isFinish ? 0.3 : 1) 749 .border({ width: 1 }) 750 .borderColor(Color.White) 751 .borderRadius(25) 752 .backgroundColor(Color.White) 753 } 754 } 755 ``` 756 757 * TodoComponent.ets 758 759 ```typescript 760 @Component 761 export struct TodoComponent { 762 build() { 763 Row() { 764 Text('To-Dos') 765 .fontSize(30) 766 .fontWeight(FontWeight.Bold) 767 } 768 .padding({ left: 15 }) 769 .width('50%') 770 .margin({ top: 10, bottom: 10 }) 771 } 772 } 773 ``` 774 775 * TodoListComponent.ets 776 777 ```typescript 778 import ThingsViewModel from "../ViewModel/ThingsViewModel"; 779 import { ThingsViewModelArray } from "../ViewModel/TodoListViewModel" 780 import { ThingsComponent } from "./ThingsComponent"; 781 782 @Component 783 export struct TodoListComponent { 784 @ObjectLink thingsViewModelArray: ThingsViewModelArray; 785 786 build() { 787 Column() { 788 List() { 789 ForEach(this.thingsViewModelArray, (item: ThingsViewModel) => { 790 // To-Do list 791 ListItem() { 792 ThingsComponent({ things: item }) 793 .margin(5) 794 } 795 }, (item: ThingsViewModel) => { 796 return item.thingsName; 797 }) 798 } 799 } 800 } 801 } 802 ``` 803 804 * ThingsViewModel.ets 805 806 ```typescript 807 import ThingsModel from "../Model/ThingsModel"; 808 809 @Observed 810 export default class ThingsViewModel { 811 @Track thingsName: string = 'Todo'; 812 @Track isFinish: boolean = false; 813 814 updateTask(things: ThingsModel) { 815 this.thingsName = things.thingsName; 816 this.isFinish = things.isFinish; 817 } 818 819 updateIsFinish(): void { 820 this.isFinish = !this.isFinish; 821 } 822 823 addSuffixes(): void { 824 this.thingsName += '!'; 825 } 826 } 827 ``` 828 829 * TodoListViewModel.ets 830 831 ```typescript 832 import ThingsViewModel from "./ThingsViewModel"; 833 import { common } from "@kit.AbilityKit"; 834 import TodoListModel from "../Model/TodoListModel"; 835 836 @Observed 837 export class ThingsViewModelArray extends Array<ThingsViewModel> { 838 } 839 840 @Observed 841 export default class TodoListViewModel { 842 @Track isChoosen: boolean = true; 843 @Track things: ThingsViewModelArray = new ThingsViewModelArray(); 844 845 async loadTasks(context: common.UIAbilityContext) { 846 let todoList = new TodoListModel([]); 847 await todoList.loadTasks(context); 848 for(let things of todoList.things) { 849 let thingsViewModel = new ThingsViewModel(); 850 thingsViewModel.updateTask(things); 851 this.things.push(thingsViewModel); 852 } 853 } 854 855 chooseAll(): void { 856 for(let things of this.things) { 857 things.isFinish = this.isChoosen; 858 } 859 this.isChoosen = !this.isChoosen; 860 } 861 } 862 ``` 863 864 * defaultTasks.json 865 866 ```typescript 867 [ 868 {"thingsName": "7:30 Get up," "isFinish": false}, 869 {"thingsName": "8:30 Breakfast," "isFinish": false}, 870 {"thingsName": "11:30 Lunch," "isFinish": false}, 871 {"thingsName": "17:30 Dinner," "isFinish": false}, 872 {"thingsName": "21:30 Snack," "isFinish": false}, 873 {"thingsName": "22:30 Shower," "isFinish": false}, 874 {"thingsName": "1:30 Go to bed," "isFinish": false} 875 ] 876 ``` 877 878 After the code is split in MVVM mode, the project structure and the responsibilities of each module are clearer. If a new page needs to use an event component, for example, **TodoListComponent**, you only need to import the component. 879 880 The following figure shows the effect. 881 882  883 884 885 886 887