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