• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1# MVVM (V2)
2<!--Kit: ArkUI-->
3<!--Subsystem: ArkUI-->
4<!--Owner: @katabanga-->
5<!--Designer: @s10021109-->
6<!--Tester: @TerryTsao-->
7<!--Adviser: @zhang_yixin13-->
8
9## Overview
10
11During 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.
12
13- 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.
14- 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.
15- ViewModel: manages UI state and interaction logic. As a bridge between Model and View, ViewModel monitors data changes in Model, notifies views to update the UI, processes user interaction events, and converts the events into data operations.
16
17## Implementing ViewModel Through V2
18
19In the MVVM mode, ViewModel manages data state and automatically updates views when data changes. The state management of V2 (referred to as V2) in ArkUI provides various decorators and tools to help you share data between custom components and ensure that data changes are automatically synchronized to the UI. Common state management decorators include \@Local, \@Param, \@Event, \@ObservedV2, and \@Trace. In addition, V2 provides **AppStorageV2** and **PersistenceV2** as global state storage tools for state sharing between applications and persistent storage.
20
21This section uses a simple to-do list as an example to introduce the decorators and tools of V2 and gradually extend functions based on a basic static to-do list. With step-by-step extension, you can gradually understand and grasp the usage of each decorator.
22
23### Basic Example
24
25First, start with a static to-do list with no state change or dynamic interaction.
26
27**Example 1**
28
29```ts
30// src/main/ets/pages/1-Basic.ets
31
32@Entry
33@ComponentV2
34struct TodoList {
35  build() {
36    Column() {
37      Text('To-Dos')
38        .fontSize(40)
39        .margin({ bottom: 10 })
40      Text('Task1')
41      Text('Task2')
42      Text('Task3')
43    }
44  }
45}
46```
47
48### Implementing Component State Observation with \@Local
49
50After the static to-do list is displayed, it needs to respond to interactions and be dynamically updated so that users can change the task completion status. To achieve this, the \@Local decorator is used to manage the component's internal state: When the variable decorated with \@Local changes, the bound UI component is re-rendered.
51
52In Example 2, an \@Local decorated **isFinish** property is added to indicate whether the task is finished. Two icons, **finished.png** and **unfinished.png**, display the task status. When a user taps a to-do item, the **isFinish** state changes, updating the icon and adding a strikethrough to the task text.
53
54**Example 2**
55
56```ts
57// src/main/ets/pages/2-Local.ets
58
59@Entry
60@ComponentV2
61struct TodoList {
62  @Local isFinish: boolean = false;
63
64  build() {
65    Column() {
66      Text('To-Dos')
67        .fontSize(40)
68        .margin({ bottom: 10 })
69      Row() {
70        // To avoid runtime errors, you must add the finished.png and unfinished.png images to the src/main/resources/base/media directory.
71        Image(this.isFinish ? $r('app.media.finished') : $r('app.media.unfinished'))
72          .width(28)
73          .height(28)
74        Text('Task1')
75          .decoration({ type: this.isFinish ? TextDecorationType.LineThrough : TextDecorationType.None })
76      }
77      .onClick(() => this.isFinish = !this.isFinish)
78    }
79  }
80}
81```
82
83### Implementing Component Input with \@Param
84After implementing local task status switching, you can enhance the to-do list's flexibility by dynamically setting each task's name (instead of hardcoding it). The \@Param decorator enables this purpose: Variables decorated with @Param in a child component can receive values passed from the parent component, implementing one-way data synchronization. By default, \@Param is read-only. To locally update the input value in the child component, combine \@Param with \@Once.
85
86In Example 3, each to-do item is abstracted into a **TaskItem** component. The \@Param decorated **taskName** attribute receives the task name from the parent **TodoList** component. This makes the **TaskItem** component flexible and reusable, as it can accept and render different task names. In addition, the **isFinish** property, decorated with both \@Param and \@Once, can be updated locally in the child component after receiving its initial value.
87
88**Example 3**
89
90```ts
91// src/main/ets/pages/3-Param.ets
92
93@ComponentV2
94struct TaskItem {
95  @Param taskName: string = '';
96  @Param @Once isFinish: boolean = false;
97
98  build() {
99    Row() {
100      // To avoid runtime errors, you must add the finished.png and unfinished.png images to the src/main/resources/base/media directory.
101      Image(this.isFinish ? $r('app.media.finished') : $r('app.media.unfinished'))
102        .width(28)
103        .height(28)
104      Text(this.taskName)
105        .decoration({ type: this.isFinish ? TextDecorationType.LineThrough : TextDecorationType.None })
106    }
107    .onClick(() => this.isFinish = !this.isFinish)
108  }
109}
110
111@Entry
112@ComponentV2
113struct TodoList {
114  build() {
115    Column() {
116      Text('To-Dos')
117        .fontSize(40)
118        .margin({ bottom: 10 })
119      TaskItem({ taskName: 'Task 1', isFinish: false })
120      TaskItem({ taskName: 'Task 2', isFinish: false })
121      TaskItem({ taskName: 'Task 3', isFinish: false })
122    }
123  }
124}
125```
126
127### Implementing Component Output with \@Event
128
129With dynamic task names now supported, the task list content remains fixed. To enable dynamic expansion of the task list, we need to add task creation and deletion functionality. This is where the \@Event decorator comes in. It allows child components to output data to parent components.
130
131In Example 4, a delete button is added to each task item, and a feature to add new tasks is included at the bottom of the list. When the delete button in the child **TaskItem** component is clicked, a **deleteTask** event is triggered and passed to the parent **TodoList** component. The parent component then responds by removing the corresponding task from the list. By combining \@Param (for receiving data) and \@Event (for passing events), child components can achieve two-way data synchronization with parent components, receiving input and sending outputs.
132
133**Example 4**
134
135```ts
136// src/main/ets/pages/4-Event.ets
137
138@ComponentV2
139struct TaskItem {
140  @Param taskName: string = '';
141  @Param @Once isFinish: boolean = false;
142  @Event deleteTask: () => void = () => {};
143
144  build() {
145    Row() {
146      // To avoid runtime errors, you must add the finished.png and unfinished.png images to the src/main/resources/base/media directory.
147      Image(this.isFinish ? $r('app.media.finished') : $r('app.media.unfinished'))
148        .width(28)
149        .height(28)
150      Text(this.taskName)
151        .decoration({ type: this.isFinish ? TextDecorationType.LineThrough : TextDecorationType.None })
152      Button('Delete')
153        .onClick(() => this.deleteTask())
154    }
155    .onClick(() => this.isFinish = !this.isFinish)
156  }
157}
158
159@Entry
160@ComponentV2
161struct TodoList {
162  @Local tasks: string[] = ['task1','task2','task3'];
163  @Local newTaskName: string = '';
164  build() {
165    Column() {
166      Text('To-Dos')
167        .fontSize(40)
168        .margin({ bottom: 10 })
169      ForEach(this.tasks, (task: string) => {
170          TaskItem({
171            taskName: task,
172            isFinish: false,
173            deleteTask: () => this.tasks.splice(this.tasks.indexOf(task), 1)
174          })
175      })
176      Row() {
177        TextInput({ placeholder: 'Add a new task', text: this.newTaskName })
178          .onChange((value) => this.newTaskName = value)
179          .width('70%')
180        Button('+')
181          .onClick(() => {
182            this.tasks.push(this.newTaskName);
183            this.newTaskName = '';
184          })
185      }
186    }
187  }
188}
189```
190
191### Implementing Component Reuse with Repeat
192
193After the task creation and deletion functionality is added, you may want to efficiently render multiple identical child components as the task list grows. This is where **Repeat** comes in handy.
194
195**Repeat** provides optimized list rendering with two distinct modes:
196- Lazy loading mode: suitable for large datasets. It loads components on demand within scrollable containers, significantly reducing memory usage and improving rendering efficiency.
197- Eager loading mode: ideal for small datasets. It renders all components at once and only updates changed items when data updates, avoiding unnecessary full re-renders.
198
199In Example 5, the eager loading mode is used due to the small number of task items. A **tasks** array is created, and the **Repeat** method iterates over each item in the array to dynamically generate and reuse **TaskItem** components. This approach enables efficient reuse of existing components during task additions or deletions, reducing redundant re-renders and improving code reusability and rendering efficiency.
200
201**Example 5**
202
203```ts
204// src/main/ets/pages/5-Repeat.ets
205
206@ComponentV2
207struct TaskItem {
208  @Param taskName: string = '';
209  @Param @Once isFinish: boolean = false;
210  @Event deleteTask: () => void = () => {};
211
212  build() {
213    Row() {
214      // To avoid runtime errors, you must add the finished.png and unfinished.png images to the src/main/resources/base/media directory.
215      Image(this.isFinish ? $r('app.media.finished') : $r('app.media.unfinished'))
216        .width(28)
217        .height(28)
218      Text(this.taskName)
219        .decoration({ type: this.isFinish ? TextDecorationType.LineThrough : TextDecorationType.None })
220      Button('Delete')
221        .onClick(() => this.deleteTask())
222    }
223    .onClick(() => this.isFinish = !this.isFinish)
224  }
225}
226
227@Entry
228@ComponentV2
229struct TodoList {
230  @Local tasks: string[] = ['task1','task2','task3'];
231  @Local newTaskName: string = '';
232  build() {
233    Column() {
234      Text('To-Dos')
235        .fontSize(40)
236        .margin({ bottom: 10 })
237      Repeat<string>(this.tasks)
238        .each((obj: RepeatItem<string>) => {
239          TaskItem({
240            taskName: obj.item,
241            isFinish: false,
242            deleteTask: () => this.tasks.splice(this.tasks.indexOf(obj.item), 1)
243          })
244        })
245      Row() {
246        TextInput({ placeholder: 'Add a new task', text: this.newTaskName })
247          .onChange((value) => this.newTaskName = value)
248          .width('70%')
249        Button('+')
250          .onClick(() => {
251            this.tasks.push(this.newTaskName);
252            this.newTaskName = '';
253          })
254      }
255    }
256  }
257}
258```
259
260### Implementing Deep Property Observation with \@ObservedV2 and \@Trace
261
262As more features are implemented, task list management becomes increasingly complex. To better handle task data changes—especially in multi-level nested structures—you must ensure that property changes can be deeply observed and trigger automatic UI re-renders. This is where the \@ObservedV2 and \@Trace decorators come into play. Unlike \@Local (which only observes changes to the object itself and its first-level properties), \@ObservedV2 and \@Trace are better suited for complex scenarios involving multi-level nesting and inheritance. In a class decorated with @ObservedV2, when a property decorated with @Trace changes, the UI component bound to that property is re-rendered automatically.
263
264In Example 6, the **Task** class is abstracted and decorated with \@ObservedV2, while its **isFinish** property is decorated with \@Trace. The **Task** class is nested within the **TaskItem** component, which in turn is nested within the **TodoList** component. At the outermost **TodoList** level, **All Finished** and **All Unfinished** buttons are added. Clicking these buttons directly updates the **isFinish** property of the innermost **Task** class instances. Thanks to \@ObservedV2 and \@Trace, these changes are detected, triggering re-renders of the UI components bound to **isFinish**, thereby enabling deep observation of nested class properties.
265
266**Example 6**
267
268```ts
269// src/main/ets/pages/6-ObservedV2Trace.ets
270
271@ObservedV2
272class Task {
273  taskName: string = '';
274  @Trace isFinish: boolean = false;
275
276  constructor (taskName: string, isFinish: boolean) {
277    this.taskName = taskName;
278    this.isFinish = isFinish;
279  }
280}
281
282@ComponentV2
283struct TaskItem {
284  @Param task: Task = new Task('', false);
285  @Event deleteTask: () => void = () => {};
286
287  build() {
288    Row() {
289      // To avoid runtime errors, you must add the finished.png and unfinished.png images to the src/main/resources/base/media directory.
290      Image(this.task.isFinish ? $r('app.media.finished') : $r('app.media.unfinished'))
291        .width(28)
292        .height(28)
293      Text(this.task.taskName)
294        .decoration({ type: this.task.isFinish ? TextDecorationType.LineThrough : TextDecorationType.None })
295      Button('Delete')
296        .onClick(() => this.deleteTask())
297    }
298    .onClick(() => this.task.isFinish = !this.task.isFinish)
299  }
300}
301
302@Entry
303@ComponentV2
304struct TodoList {
305  @Local tasks: Task[] = [
306    new Task('task1', false),
307    new Task('task2', false),
308    new Task('task3', false),
309  ];
310  @Local newTaskName: string = '';
311
312  finishAll(ifFinish: boolean) {
313    for (let task of this.tasks) {
314      task.isFinish = ifFinish;
315    }
316  }
317
318  build() {
319    Column() {
320      Text('To-Dos')
321        .fontSize(40)
322        .margin({ bottom: 10 })
323      Repeat<Task>(this.tasks)
324        .each((obj: RepeatItem<Task>) => {
325          TaskItem({
326            task: obj.item,
327            deleteTask: () => this.tasks.splice(this.tasks.indexOf(obj.item), 1)
328          })
329        })
330      Row() {
331        Button('All Finished')
332          .onClick(() => this.finishAll(true))
333        Button('All Unfinished')
334          .onClick(() => this.finishAll(false))
335      }
336      Row() {
337        TextInput({ placeholder: 'Add a new task', text: this.newTaskName })
338          .onChange((value) => this.newTaskName = value)
339          .width('70%')
340        Button('+')
341          .onClick(() => {
342            this.tasks.push(new Task(this.newTaskName, false));
343            this.newTaskName = '';
344          })
345      }
346    }
347  }
348}
349```
350
351### Implementing State Listening and Computed Properties with \@Monitor and \@Computed
352
353Building on the current task list functionality, you can enhance the user experience with additional features, such as listening for task status changes and dynamically calculating the number of unfinished tasks. This is where the \@Monitor and \@Computed decorators prove useful: \@Monitor is used to listen for deep changes in state variables, triggering custom callback methods when properties are modified. \@Computed decorates getter methods to detect changes in computed properties. It recalculates the value only once when dependencies change, reducing the overhead of redundant computations.
354
355In Example 7, \@Monitor is applied to listen for deep changes in the **isFinish** property of the **task** object within **TaskItem**. When the task status changes, the **onTasksFinished** callback is invoked to log the update. In addition, the number of unfinished tasks in **TodoList** is recorded using \@Computed to decorate **tasksUnfinished**, whose value automatically recalculates whenever task statuses change. Together, these two decorators enable deep listening of state variables and efficient computation of derived properties.
356
357**Example 7**
358
359```ts
360// src/main/ets/pages/7-MonitorComputed.ets
361
362@ObservedV2
363class Task {
364  taskName: string = '';
365  @Trace isFinish: boolean = false;
366
367  constructor (taskName: string, isFinish: boolean) {
368    this.taskName = taskName;
369    this.isFinish = isFinish;
370  }
371}
372
373@ComponentV2
374struct TaskItem {
375  @Param task: Task = new Task('', false);
376  @Event deleteTask: () => void = () => {};
377  @Monitor('task.isFinish')
378  onTaskFinished(mon: IMonitor) {
379    console.log('The status of' + this.task.taskName + 'has changed from' + mon.value()?.before + 'to' + mon.value()?.now);
380  }
381
382  build() {
383    Row() {
384      // To avoid runtime errors, you must add the finished.png and unfinished.png images to the src/main/resources/base/media directory.
385      Image(this.task.isFinish ? $r('app.media.finished') : $r('app.media.unfinished'))
386        .width(28)
387        .height(28)
388      Text(this.task.taskName)
389        .decoration({ type: this.task.isFinish ? TextDecorationType.LineThrough : TextDecorationType.None })
390      Button('Delete')
391        .onClick(() => this.deleteTask())
392    }
393    .onClick(() => this.task.isFinish = !this.task.isFinish)
394  }
395}
396
397@Entry
398@ComponentV2
399struct TodoList {
400  @Local tasks: Task[] = [
401    new Task('task1', false),
402    new Task('task2', false),
403    new Task('task3', false),
404  ];
405  @Local newTaskName: string = '';
406
407  finishAll(ifFinish: boolean) {
408    for (let task of this.tasks) {
409      task.isFinish = ifFinish;
410    }
411  }
412
413  @Computed
414  get tasksUnfinished(): number {
415    return this.tasks.filter(task => !task.isFinish).length;
416  }
417
418  build() {
419    Column() {
420      Text('To-Dos')
421        .fontSize(40)
422        .margin({ bottom: 10 })
423      Text(`Unfinished: ${this.tasksUnfinished}`)
424      Repeat<Task>(this.tasks)
425        .each((obj: RepeatItem<Task>) => {
426          TaskItem({
427            task: obj.item,
428            deleteTask: () => this.tasks.splice(this.tasks.indexOf(obj.item), 1)
429          })
430        })
431      Row() {
432        Button('All Finished')
433          .onClick(() => this.finishAll(true))
434        Button('All Unfinished')
435          .onClick(() => this.finishAll(false))
436      }
437      Row() {
438        TextInput({ placeholder: 'Add a new task', text: this.newTaskName })
439          .onChange((value) => this.newTaskName = value)
440          .width('70%')
441        Button('+')
442          .onClick(() => {
443            this.tasks.push(new Task(this.newTaskName, false));
444            this.newTaskName = '';
445          })
446      }
447    }
448  }
449}
450```
451
452### Implementing Global UI State Storage with AppStorageV2
453
454As the to-do list functionality continues to expand, an application may involve multiple pages or functional modules that require access to shared global state. For example, a to-do list application might include a settings page linked to the home page, which requires cross-page state sharing. To address this, use AppStorageV2, which stores and shares an application's global state across multiple UIAbility instances.
455
456In Example 8, a **SettingAbility** is added to load **SettingPage**, which contains a **Setting** class. This class includes a **showCompletedTask** property that controls whether finished tasks are displayed, with a switch allowing users to modify the setting. The two abilities share data through AppStorageV2 using the key **Setting**, where the corresponding data is an instance of the **Setting** class. When AppStorageV2 first connects to **Setting**, if no stored data exists, it creates a **Setting** instance by default, with **showCompletedTask** set to **true**. After users adjust settings on **SettingPage**, the task list on the home page updates accordingly. With **AppStorageV2**, data can be shared across abilities and pages.
457
458**Example 8**
459
460```ts
461// src/main/ets/pages/8-AppStorageV2.ets
462
463import { AppStorageV2 } from '@kit.ArkUI';
464import { common, Want } from '@kit.AbilityKit';
465import { Setting } from './SettingPage';
466
467@ObservedV2
468class Task {
469  taskName: string = '';
470  @Trace isFinish: boolean = false;
471
472  constructor (taskName: string, isFinish: boolean) {
473    this.taskName = taskName;
474    this.isFinish = isFinish;
475  }
476}
477
478@ComponentV2
479struct TaskItem {
480  @Param task: Task = new Task('', false);
481  @Event deleteTask: () => void = () => {};
482  @Monitor('task.isFinish')
483  onTaskFinished(mon: IMonitor) {
484    console.log('The status of' + this.task.taskName + 'has changed from' + mon.value()?.before + 'to' + mon.value()?.now);
485  }
486
487  build() {
488    Row() {
489      // To avoid runtime errors, you must add the finished.png and unfinished.png images to the src/main/resources/base/media directory.
490      Image(this.task.isFinish ? $r('app.media.finished') : $r('app.media.unfinished'))
491        .width(28)
492        .height(28)
493      Text(this.task.taskName)
494        .decoration({ type: this.task.isFinish ? TextDecorationType.LineThrough : TextDecorationType.None })
495      Button('Delete')
496        .onClick(() => this.deleteTask())
497    }
498    .onClick(() => this.task.isFinish = !this.task.isFinish)
499  }
500}
501
502@Entry
503@ComponentV2
504struct TodoList {
505  @Local tasks: Task[] = [
506    new Task('task1', false),
507    new Task('task2', false),
508    new Task('task3', false),
509  ];
510  @Local newTaskName: string = '';
511  @Local setting: Setting = AppStorageV2.connect(Setting, 'Setting', () => new Setting())!;
512  private context = this.getUIContext().getHostContext() as common.UIAbilityContext;
513
514  finishAll(ifFinish: boolean) {
515    for (let task of this.tasks) {
516      task.isFinish = ifFinish;
517    }
518  }
519
520  @Computed
521  get tasksUnfinished(): number {
522    return this.tasks.filter(task => !task.isFinish).length;
523  }
524
525  build() {
526    Column() {
527      Text('To-Dos')
528        .fontSize(40)
529        .margin({ bottom: 10 })
530      Text(`Unfinished: ${this.tasksUnfinished}`)
531      Repeat<Task>(this.tasks.filter(task => this.setting.showCompletedTask || !task.isFinish))
532        .each((obj: RepeatItem<Task>) => {
533          TaskItem({
534            task: obj.item,
535            deleteTask: () => this.tasks.splice(this.tasks.indexOf(obj.item), 1)
536          })
537        })
538      Row() {
539        Button('All Finished')
540          .onClick(() => this.finishAll(true))
541        Button('All Unfinished')
542          .onClick(() => this.finishAll(false))
543        Button('Settings')
544          .onClick(() => {
545            let wantInfo: Want = {
546              deviceId: '', // An empty deviceId indicates the local device.
547              bundleName: 'com.samples.statemgmtv2mvvm', // Replace it with the bundle name in AppScope/app.json5.
548              abilityName: 'SettingAbility',
549            };
550            this.context.startAbility(wantInfo);
551          })
552      }
553      Row() {
554        TextInput({ placeholder: 'Add a new task', text: this.newTaskName })
555          .onChange((value) => this.newTaskName = value)
556          .width('70%')
557        Button('+')
558          .onClick(() => {
559            this.tasks.push(new Task(this.newTaskName, false));
560            this.newTaskName = '';
561          })
562      }
563    }
564  }
565}
566```
567
568```ts
569// SettingPage code of the SettingAbility
570import { AppStorageV2 } from '@kit.ArkUI';
571import { common } from '@kit.AbilityKit';
572
573@ObservedV2
574export class Setting {
575  @Trace showCompletedTask: boolean = true;
576}
577
578@Entry
579@ComponentV2
580struct SettingPage {
581  @Local setting: Setting = AppStorageV2.connect(Setting, 'Setting', () => new Setting())!;
582  private context = this.getUIContext().getHostContext() as common.UIAbilityContext;
583
584  build() {
585    Column() {
586      Text('Settings')
587        .fontSize(40)
588        .margin({ bottom: 10 })
589      Row() {
590        Text('Show finished');
591        Toggle({ type: ToggleType.Switch, isOn:this.setting.showCompletedTask })
592          .onChange((isOn) => {
593            this.setting.showCompletedTask = isOn;
594          })
595      }
596      Button('Back')
597        .onClick(()=>this.context.terminateSelf())
598        .margin({ top: 10 })
599    }
600    .alignItems(HorizontalAlign.Start)
601  }
602}
603```
604
605### Implementing Persistent UI State with PersistenceV2
606
607To ensure users can view their previous task states after restarting the application, a persistent storage solution is needed. PersistenceV2 enables data to be stored persistently on the device's disk. Unlike AppStorageV2, which uses runtime memory, PersistenceV2 ensures data remains intact even after the application is restarted.
608
609In Example 9, a **TaskList** class is created to store all task information persistently through PersistenceV2, using the key **TaskList** (with the corresponding data being an instance of the **TaskList** class). When PersistenceV2 connects to **TaskList** for the first time, if no existing data is found, it initializes a **TaskList** instance with an empty tasks array by default. In the **aboutToAppear** lifecycle function, if **TaskList** connected to PersistenceV2 contains no task data, tasks are loaded from the local file **defaultTasks.json** and stored in PersistenceV2. Subsequent changes to each task's completion status are synchronized to PersistenceV2. This ensures all task data remains unchanged even after the application is restarted, achieving persistent storage of the application's state.
610
611**Example 9**
612
613```ts
614// src/main/ets/pages/9-PersistenceV2.ets
615
616import { AppStorageV2, PersistenceV2, Type } from '@kit.ArkUI';
617import { common, Want } from '@kit.AbilityKit';
618import { Setting } from './SettingPage';
619import util from '@ohos.util';
620
621@ObservedV2
622class Task {
623  // The constructor is not implemented because @Type does not support constructors with parameters.
624  @Trace taskName: string = 'Todo';
625  @Trace isFinish: boolean = false;
626}
627
628@ObservedV2
629class TaskList {
630  // For complex objects, use the @Type decorator to ensure successful serialization.
631  @Type(Task)
632  @Trace tasks: Task[] = [];
633
634  constructor(tasks: Task[]) {
635    this.tasks = tasks;
636  }
637
638  async loadTasks(context: common.UIAbilityContext) {
639    let getJson = await context.resourceManager.getRawFileContent('defaultTasks.json');
640    let textDecoderOptions: util.TextDecoderOptions = { ignoreBOM : true };
641    let textDecoder = util.TextDecoder.create('utf-8',textDecoderOptions);
642    let result = textDecoder.decodeToString(getJson);
643    this.tasks =JSON.parse(result).map((task: Task)=>{
644      let newTask = new Task();
645      newTask.taskName = task.taskName;
646      newTask.isFinish = task.isFinish;
647      return newTask;
648    });
649  }
650}
651
652@ComponentV2
653struct TaskItem {
654  @Param task: Task = new Task();
655  @Event deleteTask: () => void = () => {};
656  @Monitor('task.isFinish')
657  onTaskFinished(mon: IMonitor) {
658    console.log('The status of' + this.task.taskName + 'has changed from' + mon.value()?.before + 'to' + mon.value()?.now);
659  }
660
661  build() {
662    Row() {
663      // To avoid runtime errors, you must add the finished.png and unfinished.png images to the src/main/resources/base/media directory.
664      Image(this.task.isFinish ? $r('app.media.finished') : $r('app.media.unfinished'))
665        .width(28)
666        .height(28)
667      Text(this.task.taskName)
668        .decoration({ type: this.task.isFinish ? TextDecorationType.LineThrough : TextDecorationType.None })
669      Button('Delete')
670        .onClick(() => this.deleteTask())
671    }
672    .onClick(() => this.task.isFinish = !this.task.isFinish)
673  }
674}
675
676@Entry
677@ComponentV2
678struct TodoList {
679  @Local taskList: TaskList = PersistenceV2.connect(TaskList, 'TaskList', () => new TaskList([]))!;
680  @Local newTaskName: string = '';
681  @Local setting: Setting = AppStorageV2.connect(Setting, 'Setting', () => new Setting())!;
682  private context = this.getUIContext().getHostContext() as common.UIAbilityContext;
683
684  async aboutToAppear() {
685    this.taskList = PersistenceV2.connect(TaskList, 'TaskList', () => new TaskList([]))!;
686    if (this.taskList.tasks.length === 0) {
687      await this.taskList.loadTasks(this.context);
688    }
689  }
690
691  finishAll(ifFinish: boolean) {
692    for (let task of this.taskList.tasks) {
693      task.isFinish = ifFinish;
694    }
695  }
696
697  @Computed
698  get tasksUnfinished(): number {
699    return this.taskList.tasks.filter(task => !task.isFinish).length;
700  }
701
702  build() {
703    Column() {
704      Text('To-Dos')
705        .fontSize(40)
706        .margin({ bottom: 10 })
707      Text(`Unfinished: ${this.tasksUnfinished}`)
708      Repeat<Task>(this.taskList.tasks.filter(task => this.setting.showCompletedTask || !task.isFinish))
709        .each((obj: RepeatItem<Task>) => {
710          TaskItem({
711            task: obj.item,
712            deleteTask: () => this.taskList.tasks.splice(this.taskList.tasks.indexOf(obj.item), 1)
713          })
714        })
715      Row() {
716        Button('All Finished')
717          .onClick(() => this.finishAll(true))
718        Button('All Unfinished')
719          .onClick(() => this.finishAll(false))
720        Button('Settings')
721          .onClick(() => {
722            let wantInfo: Want = {
723              deviceId: '', // An empty deviceId indicates the local device.
724              bundleName: 'com.samples.statemgmtv2mvvm', // Replace it with the bundle name in AppScope/app.json5.
725              abilityName: 'SettingAbility',
726            };
727            this.context.startAbility(wantInfo);
728          })
729      }
730      Row() {
731        TextInput({ placeholder: 'Add a new task', text: this.newTaskName })
732          .onChange((value) => this.newTaskName = value)
733          .width('70%')
734        Button('+')
735          .onClick(() => {
736            let newTask = new Task();
737            newTask.taskName = this.newTaskName;
738            this.taskList.tasks.push(newTask);
739            this.newTaskName = '';
740          })
741      }
742    }
743  }
744}
745```
746
747The **defaultTasks.json** file is stored in **src/main/resources/rawfile** directory.
748```json
749[
750  {"taskName": "Learn to develop in ArkTS", "isFinish": false},
751  {"taskName": "Exercise", "isFinish": false},
752  {"taskName": "Buy some fruits", "isFinish": true},
753  {"taskName": "Take a delivery", "isFinish": true},
754  {"taskName": "Study", "isFinish": true}
755]
756```
757
758### Implementing Custom UI Components with \@Builder
759
760As application features expand, repeated UI elements in the code can increase volume and complicate maintenance. To address this, you can use the \@Builder decorator to abstract repetitive UI components into independent builder methods, facilitating reuse and code modularization.
761
762In Example 10, \@Builder is used to define an **ActionButton** API that unifies the management of text, styles, and touch events for various buttons. This simplifies the code and enhances maintainability. In addition, \@Builder enables adjustments to component layouts and styles, such as spacing, colors, and sizes, making the to-do list UI more visually appealing. The result is a fully functional to-do list application with a user-friendly UI.
763
764**Example 10**
765
766```ts
767// src/main/ets/pages/10-Builder.ets
768
769import { AppStorageV2, PersistenceV2, Type } from '@kit.ArkUI';
770import { common, Want } from '@kit.AbilityKit';
771import { Setting } from './SettingPage';
772import util from '@ohos.util';
773
774@ObservedV2
775class Task {
776  // The constructor is not implemented because @Type does not support constructors with parameters.
777  @Trace taskName: string = 'Todo';
778  @Trace isFinish: boolean = false;
779}
780
781@Builder function ActionButton(text: string, onClick:() => void) {
782  Button(text, { buttonStyle: ButtonStyleMode.NORMAL })
783    .onClick(onClick)
784    .margin({ left: 10, right: 10, top: 5, bottom: 5 })
785}
786
787@ObservedV2
788class TaskList {
789  // For complex objects, use the @Type decorator to ensure successful serialization.
790  @Type(Task)
791  @Trace tasks: Task[] = [];
792
793  constructor(tasks: Task[]) {
794    this.tasks = tasks;
795  }
796
797  async loadTasks(context: common.UIAbilityContext) {
798    let getJson = await context.resourceManager.getRawFileContent('defaultTasks.json');
799    let textDecoderOptions: util.TextDecoderOptions = { ignoreBOM : true };
800    let textDecoder = util.TextDecoder.create('utf-8',textDecoderOptions);
801    let result = textDecoder.decodeToString(getJson);
802    this.tasks =JSON.parse(result).map((task: Task)=>{
803      let newTask = new Task();
804      newTask.taskName = task.taskName;
805      newTask.isFinish = task.isFinish;
806      return newTask;
807    });
808  }
809}
810
811@ComponentV2
812struct TaskItem {
813  @Param task: Task = new Task();
814  @Event deleteTask: () => void = () => {};
815  @Monitor('task.isFinish')
816  onTaskFinished(mon: IMonitor) {
817    console.log('The status of' + this.task.taskName + 'has changed from' + mon.value()?.before + 'to' + mon.value()?.now);
818  }
819
820  build() {
821    Row() {
822      // To avoid runtime errors, you must add the finished.png and unfinished.png images to the src/main/resources/base/media directory.
823      Image(this.task.isFinish ? $r('app.media.finished') : $r('app.media.unfinished'))
824        .width(28)
825        .height(28)
826        .margin({ left : 15, right : 10 })
827      Text(this.task.taskName)
828        .decoration({ type: this.task.isFinish ? TextDecorationType.LineThrough : TextDecorationType.None })
829        .fontSize(18)
830      ActionButton('Delete', () => this.deleteTask())
831    }
832    .height('7%')
833    .width('90%')
834    .backgroundColor('#90f1f3f5')
835    .borderRadius(25)
836    .onClick(() => this.task.isFinish = !this.task.isFinish)
837  }
838}
839
840@Entry
841@ComponentV2
842struct TodoList {
843  @Local taskList: TaskList = PersistenceV2.connect(TaskList, 'TaskList', () => new TaskList([]))!;
844  @Local newTaskName: string = '';
845  @Local setting: Setting = AppStorageV2.connect(Setting, 'Setting', () => new Setting())!;
846  private context = this.getUIContext().getHostContext() as common.UIAbilityContext;
847
848  async aboutToAppear() {
849    this.taskList = PersistenceV2.connect(TaskList, 'TaskList', () => new TaskList([]))!;
850    if (this.taskList.tasks.length === 0) {
851      await this.taskList.loadTasks(this.context);
852    }
853  }
854
855  finishAll(ifFinish: boolean) {
856    for (let task of this.taskList.tasks) {
857      task.isFinish = ifFinish;
858    }
859  }
860
861  @Computed
862  get tasksUnfinished(): number {
863    return this.taskList.tasks.filter(task => !task.isFinish).length;
864  }
865
866  build() {
867    Column() {
868      Text('To-Dos')
869        .fontSize(40)
870        .margin(10)
871      Text(`Unfinished: ${this.tasksUnfinished}`)
872        .margin({ left: 10, bottom: 10 })
873      Repeat<Task>(this.taskList.tasks.filter(task => this.setting.showCompletedTask || !task.isFinish))
874        .each((obj: RepeatItem<Task>) => {
875          TaskItem({
876            task: obj.item,
877            deleteTask: () => this.taskList.tasks.splice(this.taskList.tasks.indexOf(obj.item), 1)
878          }).margin(5)
879        })
880      Row() {
881        ActionButton('All Finished', (): void => this.finishAll(true))
882        ActionButton('All Unfinished', (): void => this.finishAll(false))
883        ActionButton('Settings', (): void => {
884          let wantInfo: Want = {
885            deviceId: '', // An empty deviceId indicates the local device.
886            bundleName: 'com.samples.statemgmtv2mvvm', // Replace it with the bundle name in AppScope/app.json5.
887            abilityName: 'SettingAbility',
888          };
889          this.context.startAbility(wantInfo);
890        })
891      }
892      .margin({ top: 10, bottom: 5 })
893      Row() {
894        TextInput({ placeholder: 'Add a new task', text: this.newTaskName })
895          .onChange((value) => this.newTaskName = value)
896          .width('70%')
897        ActionButton('+', (): void => {
898          let newTask = new Task();
899          newTask.taskName = this.newTaskName;
900          this.taskList.tasks.push(newTask);
901          this.newTaskName = '';
902        })
903      }
904    }
905    .height('100%')
906    .width('100%')
907    .alignItems(HorizontalAlign.Start)
908    .margin({ left: 15 })
909  }
910}
911```
912
913### Display Effect
914![todolist](./figures/MVVMV2-todolist.gif)
915
916## Reconstructing Code to Comply with the MVVM Architecture
917
918The previous examples implement data synchronization and UI re-rendering in the to-do list using an array of state management decorators. However, as application functionality grows more complex, the code structure becomes harder to maintain: The responsibilities of Model, View, and ViewModel are not fully separated, resulting in lingering coupling. To better organize the code and enhance maintainability, you can refactor the code using the MVVM pattern. This further separates the data layer (Model), logic layer (ViewModel), and presentation layer (View), creating a clearer, more maintainable architecture.
919
920### Reconstructed Code Structure
921```
922/src
923├── /main
924│   ├── /ets
925│   │   ├── /entryability
926│   │   ├── /model
927│   │   │   ├── TaskListModel.ets
928│   │   │   └── TaskModel.ets
929│   │   ├── /pages
930│   │   │   ├── SettingPage.ets
931│   │   │   └── TodoListPage.ets
932│   │   ├── /settingability
933│   │   ├── /view
934│   │   │   ├── BottomView.ets
935│   │   │   ├── ListView.ets
936│   │   │   └── TitleView.ets
937│   │   ├── /viewmodel
938│   │   │   ├── TaskListViewModel.ets
939│   │   │   └── TaskViewModel.ets
940│   └── /resources
941│       ├── ...
942├─── ...
943```
944
945### Model
946The Model layer manages application data and its service logic, typically interacting with backends or data storage systems. In the to-do list application, the Model layer handles task data storage and task list loading, and provides data operation APIs, with no direct involvement in UI presentation.
947
948- **TaskModel**: basic data structure of a single task, including the task name and completion status.
949
950```ts
951// src/main/ets/model/TaskModel.ets
952
953export default class TaskModel {
954  taskName: string = 'Todo';
955  isFinish: boolean = false;
956}
957```
958
959- **TaskListModel**: a set of tasks, which provides functionality to load task data from local storage.
960```ts
961// src/main/ets/model/TaskListModel.ets
962
963import { common } from '@kit.AbilityKit';
964import util from '@ohos.util';
965import TaskModel from'./TaskModel';
966
967export default class TaskListModel {
968  tasks: TaskModel[] = [];
969
970  constructor(tasks: TaskModel[]) {
971    this.tasks = tasks;
972  }
973
974  async loadTasks(context: common.UIAbilityContext){
975    let getJson = await context.resourceManager.getRawFileContent('defaultTasks.json');
976    let textDecoderOptions: util.TextDecoderOptions = { ignoreBOM : true };
977    let textDecoder = util.TextDecoder.create('utf-8',textDecoderOptions);
978    let result = textDecoder.decodeToString(getJson);
979    this.tasks =JSON.parse(result).map((task: TaskModel)=>{
980      let newTask = new TaskModel();
981      newTask.taskName = task.taskName;
982      newTask.isFinish = task.isFinish;
983      return newTask;
984    });
985  }
986}
987```
988
989### ViewModel
990
991The ViewModel layer manages UI state and service logic, acting as a bridge between the Model and View layers. It monitors changes in Model data, processes application logic, and synchronizes data to the View layer, enabling automatic UI re-renders. This layer decouples data from views, enhancing code readability and maintainability.
992
993- **TaskViewModel**: encapsulates the change logic of data and status of a single task, and listens for data changes through the state decorator.
994
995```ts
996// src/main/ets/viewmodel/TaskViewModel.ets
997
998import TaskModel from '../model/TaskModel';
999
1000@ObservedV2
1001export default class TaskViewModel {
1002  @Trace taskName: string = 'Todo';
1003  @Trace isFinish: boolean = false;
1004
1005  updateTask(task: TaskModel) {
1006    this.taskName = task.taskName;
1007    this.isFinish = task.isFinish;
1008  }
1009
1010  updateIsFinish(): void {
1011    this.isFinish = !this.isFinish;
1012  }
1013}
1014```
1015
1016- **TaskListViewModel**: encapsulates the task list and its management functionality, including loading tasks, updating task status in batches, adding tasks, and deleting tasks.
1017
1018```ts
1019// src/main/ets/viewmodel/TaskListViewModel.ets
1020
1021import { common } from '@kit.AbilityKit';
1022import { Type } from '@kit.ArkUI';
1023import TaskListModel from '../model/TaskListModel';
1024import TaskViewModel from'./TaskViewModel';
1025
1026@ObservedV2
1027export default class TaskListViewModel {
1028  @Type(TaskViewModel)
1029  @Trace tasks: TaskViewModel[] = [];
1030
1031  async loadTasks(context: common.UIAbilityContext) {
1032    let taskList = new TaskListModel([]);
1033    await taskList.loadTasks(context);
1034    for(let task of taskList.tasks){
1035      let taskViewModel = new TaskViewModel();
1036      taskViewModel.updateTask(task);
1037      this.tasks.push(taskViewModel);
1038    }
1039  }
1040
1041  finishAll(ifFinish: boolean): void {
1042    for(let task of this.tasks){
1043      task.isFinish = ifFinish;
1044    }
1045  }
1046
1047  addTask(newTask: TaskViewModel): void {
1048    this.tasks.push(newTask);
1049  }
1050
1051  removeTask(removedTask: TaskViewModel): void {
1052    this.tasks.splice(this.tasks.indexOf(removedTask), 1)
1053  }
1054}
1055```
1056
1057### View
1058
1059The View layer is responsible for application UI rendering and user interactions. It focuses solely on how to display the UI and present data, with no inclusion of service logic. All data states and logic are derived from the ViewModel layer. By receiving and rendering state data passed from the ViewModel, the View ensures strict separation of view and data.
1060
1061- **TitleView**: displays application titles and statistics about unfinished tasks.
1062
1063```ts
1064// src/main/ets/view/TitleView.ets
1065
1066@ComponentV2
1067export default struct TitleView {
1068  @Param tasksUnfinished: number = 0;
1069
1070  build() {
1071    Column() {
1072      Text('To-Dos')
1073        .fontSize(40)
1074        .margin(10)
1075      Text(`Unfinished: ${this.tasksUnfinished}`)
1076        .margin({ left: 10, bottom: 10 })
1077    }
1078  }
1079}
1080```
1081
1082- **ListView**: displays the task list and controls visibility of completed tasks based on settings. It depends on **TaskListViewModel** to obtain task data (including the task name, completion status, and delete button), which it renders using the **TaskItem** component. It also uses **TaskViewModel** and **TaskListViewModel** to handle user interactions, such as switching the task completion status and deleting a task.
1083
1084```ts
1085// src/main/ets/view/ListView.ets
1086
1087import TaskViewModel from '../viewmodel/TaskViewModel';
1088import TaskListViewModel from '../viewmodel/TaskListViewModel';
1089import { Setting } from '../pages/SettingPage';
1090import { ActionButton } from './BottomView';
1091
1092@ComponentV2
1093struct TaskItem {
1094  @Param task: TaskViewModel = new TaskViewModel();
1095  @Event deleteTask: () => void = () => {};
1096  @Monitor('task.isFinish')
1097  onTaskFinished(mon: IMonitor) {
1098    console.log('The status of' + this.task.taskName + 'has changed from' + mon.value()?.before + 'to' + mon.value()?.now);
1099  }
1100
1101  build() {
1102    Row() {
1103      // To avoid runtime errors, you must add the finished.png and unfinished.png images to the src/main/resources/base/media directory.
1104      Image(this.task.isFinish ? $r('app.media.finished') : $r('app.media.unfinished'))
1105        .width(28)
1106        .height(28)
1107        .margin({ left: 15, right: 10 })
1108      Text(this.task.taskName)
1109        .decoration({ type: this.task.isFinish ? TextDecorationType.LineThrough : TextDecorationType.None })
1110        .fontSize(18)
1111      ActionButton('Delete', () => this.deleteTask());
1112    }
1113    .height('7%')
1114    .width('90%')
1115    .backgroundColor('#90f1f3f5')
1116    .borderRadius(25)
1117    .onClick(() => this.task.updateIsFinish())
1118  }
1119}
1120
1121@ComponentV2
1122export default struct ListView {
1123  @Param taskList: TaskListViewModel = new TaskListViewModel();
1124  @Param setting: Setting = new Setting();
1125
1126  build() {
1127    Repeat<TaskViewModel>(this.taskList.tasks.filter(task => this.setting.showCompletedTask || !task.isFinish))
1128      .each((obj: RepeatItem<TaskViewModel>) => {
1129        TaskItem({
1130          task: obj.item,
1131          deleteTask: () => this.taskList.removeTask(obj.item)
1132        }).margin(5)
1133      })
1134  }
1135}
1136```
1137
1138- **BottomView**: provides buttons (**All Finished**, **All Unfinished**, and **Settings**) and the text box for adding a task. Clicking **All Finished** or **All Unfinished** triggers **TaskListViewModel** to update the status of all tasks. Clicking **Settings** opens the **SettingAbility** settings page. Adding a task through the text box triggers **TaskListViewModel** to add the task to the task list.
1139
1140```ts
1141// src/main/ets/view/BottomView.ets
1142
1143import { common, Want } from '@kit.AbilityKit';
1144import TaskViewModel from '../viewmodel/TaskViewModel';
1145import TaskListViewModel from '../viewmodel/TaskListViewModel';
1146
1147@Builder export function ActionButton(text: string, onClick:() => void) {
1148  Button(text, { buttonStyle: ButtonStyleMode.NORMAL })
1149    .onClick(onClick)
1150    .margin({ left: 10, right: 10, top: 5, bottom: 5 })
1151}
1152
1153@ComponentV2
1154export default struct BottomView {
1155  @Param taskList: TaskListViewModel = new TaskListViewModel();
1156  @Local newTaskName: string = '';
1157  private context = this.getUIContext().getHostContext() as common.UIAbilityContext;
1158
1159  build() {
1160    Column() {
1161      Row() {
1162        ActionButton('All Finished', (): void => this.taskList.finishAll(true))
1163        ActionButton('All Unfinished', (): void => this.taskList.finishAll(false))
1164        ActionButton('Settings', (): void => {
1165          let wantInfo: Want = {
1166            deviceId: '', // An empty deviceId indicates the local device.
1167            bundleName: 'com.samples.statemgmtv2mvvm', // Replace it with the bundle name in AppScope/app.json5.
1168            abilityName: 'SettingAbility',
1169          };
1170          this.context.startAbility(wantInfo);
1171        })
1172      }
1173      .margin({ top: 10, bottom: 5 })
1174      Row() {
1175        TextInput({ placeholder: 'Add a new task', text: this.newTaskName })
1176          .onChange((value) => this.newTaskName = value)
1177          .width('70%')
1178        ActionButton('+', (): void => {
1179          let newTask = new TaskViewModel();
1180          newTask.taskName = this.newTaskName;
1181          this.taskList.addTask(newTask);
1182          this.newTaskName = '';
1183        })
1184      }
1185    }
1186  }
1187}
1188```
1189
1190- **TodoListPage**: represents the main page of the to-do list and integrates the preceding three **View** components (**TitleView**, **ListView**, and **BottomView**) to unify display of all to-do list sections.It manages the task list and settings by obtaining data from ViewModel, passing the data to each child **View** component for rendering, and using PersistenceV2 to persist task data, which ensures consistency after application restarts.
1191
1192```ts
1193// src/main/ets/pages/TodoListPage.ets
1194
1195import TaskListViewModel from '../viewmodel/TaskListViewModel';
1196import { common } from '@kit.AbilityKit';
1197import { AppStorageV2, PersistenceV2 } from '@kit.ArkUI';
1198import { Setting } from '../pages/SettingPage';
1199import TitleView from '../view/TitleView';
1200import ListView from '../view/ListView';
1201import BottomView from '../view/BottomView';
1202
1203@Entry
1204@ComponentV2
1205struct TodoList {
1206  @Local taskList: TaskListViewModel = PersistenceV2.connect(TaskListViewModel, 'TaskList', () => new TaskListViewModel())!;
1207  @Local setting: Setting = AppStorageV2.connect(Setting, 'Setting', () => new Setting())!;
1208  private context = this.getUIContext().getHostContext() as common.UIAbilityContext;
1209
1210  async aboutToAppear() {
1211    this.taskList = PersistenceV2.connect(TaskListViewModel, 'TaskList', () => new TaskListViewModel())!;
1212    if (this.taskList.tasks.length === 0) {
1213      await this.taskList.loadTasks(this.context);
1214    }
1215  }
1216
1217  @Computed
1218  get tasksUnfinished(): number {
1219    return this.taskList.tasks.filter(task => !task.isFinish).length;
1220  }
1221
1222  build() {
1223    Column() {
1224      TitleView({ tasksUnfinished: this.tasksUnfinished })
1225      ListView({ taskList: this.taskList, setting: this.setting });
1226      BottomView({ taskList: this.taskList });
1227    }
1228    .height('100%')
1229    .width('100%')
1230    .alignItems(HorizontalAlign.Start)
1231    .margin({ left: 15 })
1232  }
1233}
1234```
1235
1236- **SettingPage**: represents the settings page for configuring whether to show finished tasks. It uses AppStorageV2 to store the global settings. The user can switch the status of **showCompletedTask** by using the toggle switch.
1237
1238```ts
1239// src/main/ets/pages/SettingPage.ets
1240
1241import { AppStorageV2 } from '@kit.ArkUI';
1242import { common } from '@kit.AbilityKit';
1243
1244@ObservedV2
1245export class Setting {
1246  @Trace showCompletedTask: boolean = true;
1247}
1248
1249@Entry
1250@ComponentV2
1251struct SettingPage {
1252  @Local setting: Setting = AppStorageV2.connect(Setting, 'Setting', () => new Setting())!;
1253  private context = this.getUIContext().getHostContext() as common.UIAbilityContext;
1254
1255  build(){
1256    Column(){
1257      Text('Settings')
1258        .fontSize(40)
1259        .margin({ bottom: 10 })
1260      Row() {
1261        Text('Show finished');
1262        Toggle({ type: ToggleType.Switch, isOn:this.setting.showCompletedTask })
1263          .onChange((isOn) => {
1264            this.setting.showCompletedTask = isOn;
1265          })
1266      }
1267      Button('Back')
1268        .onClick(()=>this.context.terminateSelf())
1269        .margin({ top: 10 })
1270    }
1271    .alignItems(HorizontalAlign.Start)
1272  }
1273}
1274```
1275
1276## Summary
1277
1278This guide uses a simple to-do list application to demonstrate the use of V2 decorators and the implementation of the MVVM architecture through code refactoring. By separating data, service logic, and views into distinct layers, you can achieve a clearer code structure that is easier to maintain. By correctly implementing the layered architecture of Model, View, and ViewModel, you can gain a clearer understanding of the MVVM pattern and apply it more effectively. This layered approach delivers multiple practical benefits in real-world projects: It boosts development efficiency, ensures consistent code quality, optimizes the synchronization mechanism between data and the UI, and ultimately streamlines the entire development workflow.
1279