• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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![MVVM image](./figures/MVVM_architecture.png)
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![state](./figures/MVVM_state.gif)
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![Prop&Link](./figures/MVVM_Prop&Link.gif)
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![ForEach](./figures/MVVM_ForEach.gif)
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![builder](./figures/MVVM_builder.gif)
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  ![MVVM_index.gif](./figures/MVVM_index.gif)
883
884
885
886
887