• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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// src/main/ets/pages/1-Basic.ets
24
25@Entry
26@ComponentV2
27struct TodoList {
28  build() {
29    Column() {
30      Text('To-Dos')
31        .fontSize(40)
32        .margin({ bottom: 10 })
33      Text('Task1')
34      Text('Task2')
35      Text('Task3')
36    }
37  }
38}
39```
40
41### Adding \@Local to Observe the Internal State of Components
42
43After 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.
44
45In 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.
46
47```ts
48// src/main/ets/pages/2-Local.ets
49
50@Entry
51@ComponentV2
52struct TodoList {
53  @Local isFinish: boolean = false;
54
55  build() {
56    Column() {
57      Text('To-Dos')
58        .fontSize(40)
59        .margin({ bottom: 10 })
60      Row() {
61        // 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.
62        Image(this.isFinish ? $r('app.media.finished') : $r('app.media.unfinished'))
63          .width(28)
64          .height(28)
65        Text('Task1')
66          .decoration({ type: this.isFinish ? TextDecorationType.LineThrough : TextDecorationType.None })
67      }
68      .onClick(() => this.isFinish = !this.isFinish)
69    }
70  }
71}
72```
73
74### Adding \@Param to Enable Components to Receive External Input
75After 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.
76
77In 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.
78
79```ts
80// src/main/ets/pages/3-Param.ets
81
82@ComponentV2
83struct TaskItem {
84  @Param taskName: string = '';
85  @Param @Once isFinish: boolean = false;
86
87  build() {
88    Row() {
89      // 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.
90      Image(this.isFinish ? $r('app.media.finished') : $r('app.media.unfinished'))
91        .width(28)
92        .height(28)
93      Text(this.taskName)
94        .decoration({ type: this.isFinish ? TextDecorationType.LineThrough : TextDecorationType.None })
95    }
96    .onClick(() => this.isFinish = !this.isFinish)
97  }
98}
99
100@Entry
101@ComponentV2
102struct TodoList {
103  build() {
104    Column() {
105      Text('To-Dos')
106        .fontSize(40)
107        .margin({ bottom: 10 })
108      TaskItem({ taskName: 'Task 1', isFinish: false })
109      TaskItem({ taskName: 'Task 2', isFinish: false })
110      TaskItem({ taskName: 'Task 3', isFinish: false })
111    }
112  }
113}
114```
115
116### Adding \@Event to Enable Components to Output Externally
117
118After 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.
119
120In 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.
121
122```ts
123// src/main/ets/pages/4-Event.ets
124
125@ComponentV2
126struct TaskItem {
127  @Param taskName: string = '';
128  @Param @Once isFinish: boolean = false;
129  @Event deleteTask: () => void = () => {};
130
131  build() {
132    Row() {
133      // 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.
134      Image(this.isFinish ? $r('app.media.finished') : $r('app.media.unfinished'))
135        .width(28)
136        .height(28)
137      Text(this.taskName)
138        .decoration({ type: this.isFinish ? TextDecorationType.LineThrough : TextDecorationType.None })
139      Button('Delete')
140        .onClick(() => this.deleteTask())
141    }
142    .onClick(() => this.isFinish = !this.isFinish)
143  }
144}
145
146@Entry
147@ComponentV2
148struct TodoList {
149  @Local tasks: string[] = ['task1','task2','task3'];
150  @Local newTaskName: string = '';
151  build() {
152    Column() {
153      Text('To-Dos')
154        .fontSize(40)
155        .margin({ bottom: 10 })
156      ForEach(this.tasks, (task: string) => {
157          TaskItem({
158            taskName: task,
159            isFinish: false,
160            deleteTask: () => this.tasks.splice(this.tasks.indexOf(task), 1)
161          })
162      })
163      Row() {
164        TextInput({ placeholder: 'Add a new task', text: this.newTaskName })
165          .onChange((value) => this.newTaskName = value)
166          .width('70%')
167        Button('+')
168          .onClick(() => {
169            this.tasks.push(this.newTaskName);
170            this.newTaskName = '';
171          })
172      }
173    }
174  }
175}
176```
177
178### Adding Repeat to Implement Child Component Reuse
179
180As 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.
181
182In 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. In this way, you can efficiently reuse existing components when adding or deleting a task to avoid repeated component renderings, improving code reusability and rendering efficiency.
183
184```ts
185// src/main/ets/pages/5-Repeat.ets
186
187@ComponentV2
188struct TaskItem {
189  @Param taskName: string = '';
190  @Param @Once isFinish: boolean = false;
191  @Event deleteTask: () => void = () => {};
192
193  build() {
194    Row() {
195      // 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.
196      Image(this.isFinish ? $r('app.media.finished') : $r('app.media.unfinished'))
197        .width(28)
198        .height(28)
199      Text(this.taskName)
200        .decoration({ type: this.isFinish ? TextDecorationType.LineThrough : TextDecorationType.None })
201      Button('Delete')
202        .onClick(() => this.deleteTask())
203    }
204    .onClick(() => this.isFinish = !this.isFinish)
205  }
206}
207
208@Entry
209@ComponentV2
210struct TodoList {
211  @Local tasks: string[] = ['task1','task2','task3'];
212  @Local newTaskName: string = '';
213  build() {
214    Column() {
215      Text('To-Dos')
216        .fontSize(40)
217        .margin({ bottom: 10 })
218      Repeat<string>(this.tasks)
219        .each((obj: RepeatItem<string>) => {
220          TaskItem({
221            taskName: obj.item,
222            isFinish: false,
223            deleteTask: () => this.tasks.splice(this.tasks.indexOf(obj.item), 1)
224          })
225        })
226      Row() {
227        TextInput({ placeholder: 'Add a new task', text: this.newTaskName })
228          .onChange((value) => this.newTaskName = value)
229          .width('70%')
230        Button('+')
231          .onClick(() => {
232            this.tasks.push(this.newTaskName);
233            this.newTaskName = '';
234          })
235      }
236    }
237  }
238}
239```
240
241### Adding \@ObservedV2 and \@Trace to Observe Changes of Class Properties
242
243After 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.
244
245In 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.
246
247```ts
248// src/main/ets/pages/6-ObservedV2Trace.ets
249
250@ObservedV2
251class Task {
252  taskName: string = '';
253  @Trace isFinish: boolean = false;
254
255  constructor (taskName: string, isFinish: boolean) {
256    this.taskName = taskName;
257    this.isFinish = isFinish;
258  }
259}
260
261@ComponentV2
262struct TaskItem {
263  @Param task: Task = new Task('', false);
264  @Event deleteTask: () => void = () => {};
265
266  build() {
267    Row() {
268      // 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.
269      Image(this.task.isFinish ? $r('app.media.finished') : $r('app.media.unfinished'))
270        .width(28)
271        .height(28)
272      Text(this.task.taskName)
273        .decoration({ type: this.task.isFinish ? TextDecorationType.LineThrough : TextDecorationType.None })
274      Button('Delete')
275        .onClick(() => this.deleteTask())
276    }
277    .onClick(() => this.task.isFinish = !this.task.isFinish)
278  }
279}
280
281@Entry
282@ComponentV2
283struct TodoList {
284  @Local tasks: Task[] = [
285    new Task('task1', false),
286    new Task('task2', false),
287    new Task('task3', false),
288  ];
289  @Local newTaskName: string = '';
290
291  finishAll(ifFinish: boolean) {
292    for (let task of this.tasks) {
293      task.isFinish = ifFinish;
294    }
295  }
296
297  build() {
298    Column() {
299      Text('To-Dos')
300        .fontSize(40)
301        .margin({ bottom: 10 })
302      Repeat<Task>(this.tasks)
303        .each((obj: RepeatItem<Task>) => {
304          TaskItem({
305            task: obj.item,
306            deleteTask: () => this.tasks.splice(this.tasks.indexOf(obj.item), 1)
307          })
308        })
309      Row() {
310        Button('All Finished')
311          .onClick(() => this.finishAll(true))
312        Button('All Unfinished')
313          .onClick(() => this.finishAll(false))
314      }
315      Row() {
316        TextInput({ placeholder: 'Add a new task', text: this.newTaskName })
317          .onChange((value) => this.newTaskName = value)
318          .width('70%')
319        Button('+')
320          .onClick(() => {
321            this.tasks.push(new Task(this.newTaskName, false));
322            this.newTaskName = '';
323          })
324      }
325    }
326  }
327}
328```
329
330### Adding \@Monitor and \@Computed to Listen for State Variables and Computation Properties
331
332Based 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.
333
334In 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.
335
336```ts
337// src/main/ets/pages/7-MonitorComputed.ets
338
339@ObservedV2
340class Task {
341  taskName: string = '';
342  @Trace isFinish: boolean = false;
343
344  constructor (taskName: string, isFinish: boolean) {
345    this.taskName = taskName;
346    this.isFinish = isFinish;
347  }
348}
349
350@ComponentV2
351struct TaskItem {
352  @Param task: Task = new Task('', false);
353  @Event deleteTask: () => void = () => {};
354  @Monitor('task.isFinish')
355  onTaskFinished(mon: IMonitor) {
356    console.log('The status of' + this.task.taskName + 'has changed from' + mon.value()?.before + 'to' + mon.value()?.now);
357  }
358
359  build() {
360    Row() {
361      // 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.
362      Image(this.task.isFinish ? $r('app.media.finished') : $r('app.media.unfinished'))
363        .width(28)
364        .height(28)
365      Text(this.task.taskName)
366        .decoration({ type: this.task.isFinish ? TextDecorationType.LineThrough : TextDecorationType.None })
367      Button('Delete')
368        .onClick(() => this.deleteTask())
369    }
370    .onClick(() => this.task.isFinish = !this.task.isFinish)
371  }
372}
373
374@Entry
375@ComponentV2
376struct TodoList {
377  @Local tasks: Task[] = [
378    new Task('task1', false),
379    new Task('task2', false),
380    new Task('task3', false),
381  ];
382  @Local newTaskName: string = '';
383
384  finishAll(ifFinish: boolean) {
385    for (let task of this.tasks) {
386      task.isFinish = ifFinish;
387    }
388  }
389
390  @Computed
391  get tasksUnfinished(): number {
392    return this.tasks.filter(task => !task.isFinish).length;
393  }
394
395  build() {
396    Column() {
397      Text('To-Dos')
398        .fontSize(40)
399        .margin({ bottom: 10 })
400      Text('Unfinished: ${this.tasksUnfinished}')
401      Repeat<Task>(this.tasks)
402        .each((obj: RepeatItem<Task>) => {
403          TaskItem({
404            task: obj.item,
405            deleteTask: () => this.tasks.splice(this.tasks.indexOf(obj.item), 1)
406          })
407        })
408      Row() {
409        Button('All Finished')
410          .onClick(() => this.finishAll(true))
411        Button('All Unfinished')
412          .onClick(() => this.finishAll(false))
413      }
414      Row() {
415        TextInput({ placeholder: 'Add a new task', text: this.newTaskName })
416          .onChange((value) => this.newTaskName = value)
417          .width('70%')
418        Button('+')
419          .onClick(() => {
420            this.tasks.push(new Task(this.newTaskName, false));
421            this.newTaskName = '';
422          })
423      }
424    }
425  }
426}
427```
428
429### Adding AppStorageV2 to Store Global UI State of Applications
430
431With 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.
432
433In 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.
434
435```ts
436// src/main/ets/pages/8-AppStorageV2.ets
437
438import { AppStorageV2 } from '@kit.ArkUI';
439import { common, Want } from '@kit.AbilityKit';
440import { Setting } from './SettingPage';
441
442@ObservedV2
443class Task {
444  taskName: string = '';
445  @Trace isFinish: boolean = false;
446
447  constructor (taskName: string, isFinish: boolean) {
448    this.taskName = taskName;
449    this.isFinish = isFinish;
450  }
451}
452
453@ComponentV2
454struct TaskItem {
455  @Param task: Task = new Task('', false);
456  @Event deleteTask: () => void = () => {};
457  @Monitor('task.isFinish')
458  onTaskFinished(mon: IMonitor) {
459    console.log('The status of' + this.task.taskName + 'has changed from' + mon.value()?.before + 'to' + mon.value()?.now);
460  }
461
462  build() {
463    Row() {
464      // 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.
465      Image(this.task.isFinish ? $r('app.media.finished') : $r('app.media.unfinished'))
466        .width(28)
467        .height(28)
468      Text(this.task.taskName)
469        .decoration({ type: this.task.isFinish ? TextDecorationType.LineThrough : TextDecorationType.None })
470      Button('Delete')
471        .onClick(() => this.deleteTask())
472    }
473    .onClick(() => this.task.isFinish = !this.task.isFinish)
474  }
475}
476
477@Entry
478@ComponentV2
479struct TodoList {
480  @Local tasks: Task[] = [
481    new Task('task1', false),
482    new Task('task2', false),
483    new Task('task3', false),
484  ];
485  @Local newTaskName: string = '';
486  @Local setting: Setting = AppStorageV2.connect(Setting, 'Setting', () => new Setting())!;
487  private context = getContext(this) as common.UIAbilityContext;
488
489  finishAll(ifFinish: boolean) {
490    for (let task of this.tasks) {
491      task.isFinish = ifFinish;
492    }
493  }
494
495  @Computed
496  get tasksUnfinished(): number {
497    return this.tasks.filter(task => !task.isFinish).length;
498  }
499
500  build() {
501    Column() {
502      Text('To-Dos')
503        .fontSize(40)
504        .margin({ bottom: 10 })
505      Text('Unfinished: ${this.tasksUnfinished}')
506      Repeat<Task>(this.tasks.filter(task => this.setting.showCompletedTask || !task.isFinish))
507        .each((obj: RepeatItem<Task>) => {
508          TaskItem({
509            task: obj.item,
510            deleteTask: () => this.tasks.splice(this.tasks.indexOf(obj.item), 1)
511          })
512        })
513      Row() {
514        Button('All Finished')
515          .onClick(() => this.finishAll(true))
516        Button('All Unfinished')
517          .onClick(() => this.finishAll(false))
518        Button('Settings')
519          .onClick(() => {
520            let wantInfo: Want = {
521              deviceId: '', // An empty deviceId indicates the local device.
522              bundleName: 'com.samples.statemgmtv2mvvm', // Replace it with the bundle name in AppScope/app.json5.
523              abilityName: 'SettingAbility',
524            };
525            this.context.startAbility(wantInfo);
526          })
527      }
528      Row() {
529        TextInput({ placeholder: 'Add a new task', text: this.newTaskName })
530          .onChange((value) => this.newTaskName = value)
531          .width('70%')
532        Button('+')
533          .onClick(() => {
534            this.tasks.push(new Task(this.newTaskName, false));
535            this.newTaskName = '';
536          })
537      }
538    }
539  }
540}
541```
542
543```ts
544// SettingPage code of the SettingAbility.
545import { AppStorageV2 } from '@kit.ArkUI';
546import { common } from '@kit.AbilityKit';
547
548@ObservedV2
549export class Setting {
550  @Trace showCompletedTask: boolean = true;
551}
552
553@Entry
554@ComponentV2
555struct SettingPage {
556  @Local setting: Setting = AppStorageV2.connect(Setting, 'Setting', () => new Setting())!;
557  private context = getContext(this) as common.UIAbilityContext;
558
559  build() {
560    Column() {
561      Text('Settings')
562        .fontSize(40)
563        .margin({ bottom: 10 })
564      Row() {
565        Text('Show finished');
566        Toggle({ type: ToggleType.Switch, isOn:this.setting.showCompletedTask })
567          .onChange((isOn) => {
568            this.setting.showCompletedTask = isOn;
569          })
570      }
571      Button('Back')
572        .onClick(()=>this.context.terminateSelf())
573        .margin({ top: 10 })
574    }
575    .alignItems(HorizontalAlign.Start)
576  }
577}
578```
579
580### Adding PersistenceV2 to Implement Persistent UI State Storage
581
582To 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.
583
584In 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.
585
586```ts
587// src/main/ets/pages/9-PersistenceV2.ets
588
589import { AppStorageV2, PersistenceV2, Type } from '@kit.ArkUI';
590import { common, Want } from '@kit.AbilityKit';
591import { Setting } from './SettingPage';
592import util from '@ohos.util';
593
594@ObservedV2
595class Task {
596  // The constructor is not implemented because @Type does not support constructors with parameters.
597  @Trace taskName: string = 'Todo';
598  @Trace isFinish: boolean = false;
599}
600
601@ObservedV2
602class TaskList {
603  // Complex objects need to be decorated by @Type to ensure successful serialization.
604  @Type(Task)
605  @Trace tasks: Task[] = [];
606
607  constructor(tasks: Task[]) {
608    this.tasks = tasks;
609  }
610
611  async loadTasks(context: common.UIAbilityContext) {
612    let getJson = await context.resourceManager.getRawFileContent('defaultTasks.json');
613    let textDecoderOptions: util.TextDecoderOptions = { ignoreBOM : true };
614    let textDecoder = util.TextDecoder.create('utf-8',textDecoderOptions);
615    let result = textDecoder.decodeToString(getJson);
616    this.tasks =JSON.parse(result).map((task: Task)=>{
617      let newTask = new Task();
618      newTask.taskName = task.taskName;
619      newTask.isFinish = task.isFinish;
620      return newTask;
621    });
622  }
623}
624
625@ComponentV2
626struct TaskItem {
627  @Param task: Task = new Task();
628  @Event deleteTask: () => void = () => {};
629  @Monitor('task.isFinish')
630  onTaskFinished(mon: IMonitor) {
631    console.log('The status of' + this.task.taskName + 'has changed from' + mon.value()?.before + 'to' + mon.value()?.now);
632  }
633
634  build() {
635    Row() {
636      // 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.
637      Image(this.task.isFinish ? $r('app.media.finished') : $r('app.media.unfinished'))
638        .width(28)
639        .height(28)
640      Text(this.task.taskName)
641        .decoration({ type: this.task.isFinish ? TextDecorationType.LineThrough : TextDecorationType.None })
642      Button('Delete')
643        .onClick(() => this.deleteTask())
644    }
645    .onClick(() => this.task.isFinish = !this.task.isFinish)
646  }
647}
648
649@Entry
650@ComponentV2
651struct TodoList {
652  @Local taskList: TaskList = PersistenceV2.connect(TaskList, 'TaskList', () => new TaskList([]))!;
653  @Local newTaskName: string = '';
654  @Local setting: Setting = AppStorageV2.connect(Setting, 'Setting', () => new Setting())!;
655  private context = getContext(this) as common.UIAbilityContext;
656
657  async aboutToAppear() {
658    this.taskList = PersistenceV2.connect(TaskList, 'TaskList', () => new TaskList([]))!;
659    if (this.taskList.tasks.length === 0) {
660      await this.taskList.loadTasks(this.context);
661    }
662  }
663
664  finishAll(ifFinish: boolean) {
665    for (let task of this.taskList.tasks) {
666      task.isFinish = ifFinish;
667    }
668  }
669
670  @Computed
671  get tasksUnfinished(): number {
672    return this.taskList.tasks.filter(task => !task.isFinish).length;
673  }
674
675  build() {
676    Column() {
677      Text('To-Dos')
678        .fontSize(40)
679        .margin({ bottom: 10 })
680      Text('Unfinished: ${this.tasksUnfinished}')
681      Repeat<Task>(this.taskList.tasks.filter(task => this.setting.showCompletedTask || !task.isFinish))
682        .each((obj: RepeatItem<Task>) => {
683          TaskItem({
684            task: obj.item,
685            deleteTask: () => this.taskList.tasks.splice(this.taskList.tasks.indexOf(obj.item), 1)
686          })
687        })
688      Row() {
689        Button('All Finished')
690          .onClick(() => this.finishAll(true))
691        Button('All Unfinished')
692          .onClick(() => this.finishAll(false))
693        Button('Settings')
694          .onClick(() => {
695            let wantInfo: Want = {
696              deviceId: '', // An empty deviceId indicates the local device.
697              bundleName: 'com.samples.statemgmtv2mvvm', // Replace it with the bundle name in AppScope/app.json5.
698              abilityName: 'SettingAbility',
699            };
700            this.context.startAbility(wantInfo);
701          })
702      }
703      Row() {
704        TextInput({ placeholder: 'Add a new task', text: this.newTaskName })
705          .onChange((value) => this.newTaskName = value)
706          .width('70%')
707        Button('+')
708          .onClick(() => {
709            let newTask = new Task();
710            newTask.taskName = this.newTaskName;
711            this.taskList.tasks.push(newTask);
712            this.newTaskName = '';
713          })
714      }
715    }
716  }
717}
718```
719
720The **defaultTasks.json** file is stored in **src/main/resources/rawfile** directory.
721```json
722[
723  {"taskName": "Learn to develop in ArkTS", "isFinish": false},
724  {"taskName": "Exercise", "isFinish": false},
725  {"taskName": "Buy some fruits", "isFinish": true},
726  {"taskName": "Take a delivery", "isFinish": true},
727  {"taskName": "Study", "isFinish": true}
728]
729```
730
731### Adding \@Builder to Customize a Constructor
732
733As 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.
734
735In 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.
736
737```ts
738// src/main/ets/pages/10-Builder.ets
739
740import { AppStorageV2, PersistenceV2, Type } from '@kit.ArkUI';
741import { common, Want } from '@kit.AbilityKit';
742import { Setting } from './SettingPage';
743import util from '@ohos.util';
744
745@ObservedV2
746class Task {
747  // The constructor is not implemented because @Type does not support constructors with parameters.
748  @Trace taskName: string = 'Todo';
749  @Trace isFinish: boolean = false;
750}
751
752@Builder function ActionButton(text: string, onClick:() => void) {
753  Button(text, { buttonStyle: ButtonStyleMode.NORMAL })
754    .onClick(onClick)
755    .margin({ left: 10, right: 10, top: 5, bottom: 5 })
756}
757
758@ObservedV2
759class TaskList {
760  // Complex objects need to be decorated by @Type to ensure successful serialization.
761  @Type(Task)
762  @Trace tasks: Task[] = [];
763
764  constructor(tasks: Task[]) {
765    this.tasks = tasks;
766  }
767
768  async loadTasks(context: common.UIAbilityContext) {
769    let getJson = await context.resourceManager.getRawFileContent('defaultTasks.json');
770    let textDecoderOptions: util.TextDecoderOptions = { ignoreBOM : true };
771    let textDecoder = util.TextDecoder.create('utf-8',textDecoderOptions);
772    let result = textDecoder.decodeToString(getJson);
773    this.tasks =JSON.parse(result).map((task: Task)=>{
774      let newTask = new Task();
775      newTask.taskName = task.taskName;
776      newTask.isFinish = task.isFinish;
777      return newTask;
778    });
779  }
780}
781
782@ComponentV2
783struct TaskItem {
784  @Param task: Task = new Task();
785  @Event deleteTask: () => void = () => {};
786  @Monitor('task.isFinish')
787  onTaskFinished(mon: IMonitor) {
788    console.log('The status of' + this.task.taskName + 'has changed from' + mon.value()?.before + 'to' + mon.value()?.now);
789  }
790
791  build() {
792    Row() {
793      // 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.
794      Image(this.task.isFinish ? $r('app.media.finished') : $r('app.media.unfinished'))
795        .width(28)
796        .height(28)
797        .margin({ left : 15, right : 10 })
798      Text(this.task.taskName)
799        .decoration({ type: this.task.isFinish ? TextDecorationType.LineThrough : TextDecorationType.None })
800        .fontSize(18)
801      ActionButton('Delete', () => this.deleteTask())
802    }
803    .height('7%')
804    .width('90%')
805    .backgroundColor('#90f1f3f5')
806    .borderRadius(25)
807    .onClick(() => this.task.isFinish = !this.task.isFinish)
808  }
809}
810
811@Entry
812@ComponentV2
813struct TodoList {
814  @Local taskList: TaskList = PersistenceV2.connect(TaskList, 'TaskList', () => new TaskList([]))!;
815  @Local newTaskName: string = '';
816  @Local setting: Setting = AppStorageV2.connect(Setting, 'Setting', () => new Setting())!;
817  private context = getContext(this) as common.UIAbilityContext;
818
819  async aboutToAppear() {
820    this.taskList = PersistenceV2.connect(TaskList, 'TaskList', () => new TaskList([]))!;
821    if (this.taskList.tasks.length === 0) {
822      await this.taskList.loadTasks(this.context);
823    }
824  }
825
826  finishAll(ifFinish: boolean) {
827    for (let task of this.taskList.tasks) {
828      task.isFinish = ifFinish;
829    }
830  }
831
832  @Computed
833  get tasksUnfinished(): number {
834    return this.taskList.tasks.filter(task => !task.isFinish).length;
835  }
836
837  build() {
838    Column() {
839      Text('To-Dos')
840        .fontSize(40)
841        .margin(10)
842      Text('Unfinished: ${this.tasksUnfinished}')
843        .margin({ left: 10, bottom: 10 })
844      Repeat<Task>(this.taskList.tasks.filter(task => this.setting.showCompletedTask || !task.isFinish))
845        .each((obj: RepeatItem<Task>) => {
846          TaskItem({
847            task: obj.item,
848            deleteTask: () => this.taskList.tasks.splice(this.taskList.tasks.indexOf(obj.item), 1)
849          }).margin(5)
850        })
851      Row() {
852        ActionButton('All Finished', (): void => this.finishAll(true))
853        ActionButton('All Unfinished', (): void => this.finishAll(false))
854        ActionButton('Settings', (): void => {
855          let wantInfo: Want = {
856            deviceId: '', // An empty deviceId indicates the local device.
857            bundleName: 'com.samples.statemgmtv2mvvm', // Replace it with the bundle name in AppScope/app.json5.
858            abilityName: 'SettingAbility',
859          };
860          this.context.startAbility(wantInfo);
861        })
862      }
863      .margin({ top: 10, bottom: 5 })
864      Row() {
865        TextInput({ placeholder: 'Add a new task', text: this.newTaskName })
866          .onChange((value) => this.newTaskName = value)
867          .width('70%')
868        ActionButton('+', (): void => {
869          let newTask = new Task();
870          newTask.taskName = this.newTaskName;
871          this.taskList.tasks.push(newTask);
872          this.newTaskName = '';
873        })
874      }
875    }
876    .height('100%')
877    .width('100%')
878    .alignItems(HorizontalAlign.Start)
879    .margin({ left: 15 })
880  }
881}
882```
883
884### Display Effect
885![todolist](./figures/MVVMV2-todolist.gif)
886
887## Reconstructing Code to Comply with the MVVM Architecture
888
889The 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).
890
891### Reconstructed Code Structure
892```
893/src
894├── /main
895│   ├── /ets
896│   │   ├── /entryability
897│   │   ├── /model
898│   │   │   ├── TaskListModel.ets
899│   │   │   └── TaskModel.ets
900│   │   ├── /pages
901│   │   │   ├── SettingPage.ets
902│   │   │   └── TodoListPage.ets
903│   │   ├── /settingability
904│   │   ├── /view
905│   │   │   ├── BottomView.ets
906│   │   │   ├── ListView.ets
907│   │   │   └── TitleView.ets
908│   │   ├── /viewmodel
909│   │   │   ├── TaskListViewModel.ets
910│   │   │   └── TaskViewModel.ets
911│   └── /resources
912│       ├── ...
913├─── ...
914```
915
916### Model
917The 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.
918
919- **TaskModel**: basic data structure of a single task, including the task name and completion status.
920
921```ts
922// src/main/ets/model/TaskModel.ets
923
924export default class TaskModel {
925  taskName: string = 'Todo';
926  isFinish: boolean = false;
927}
928```
929
930- **TaskListModel**: a set of tasks, which provides the function of loading task data from the local host.
931```ts
932// src/main/ets/model/TaskListModel.ets
933
934import { common } from '@kit.AbilityKit';
935import util from '@ohos.util';
936import TaskModel from'./TaskModel';
937
938export default class TaskListModel {
939  tasks: TaskModel[] = [];
940
941  constructor(tasks: TaskModel[]) {
942    this.tasks = tasks;
943  }
944
945  async loadTasks(context: common.UIAbilityContext){
946    let getJson = await context.resourceManager.getRawFileContent('defaultTasks.json');
947    let textDecoderOptions: util.TextDecoderOptions = { ignoreBOM : true };
948    let textDecoder = util.TextDecoder.create('utf-8',textDecoderOptions);
949    let result = textDecoder.decodeToString(getJson);
950    this.tasks =JSON.parse(result).map((task: TaskModel)=>{
951      let newTask = new TaskModel();
952      newTask.taskName = task.taskName;
953      newTask.isFinish = task.isFinish;
954      return newTask;
955    });
956  }
957}
958```
959
960### ViewModel
961
962The 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.
963
964- **TaskViewModel**: encapsulates the change logic of data and status of a single task, and listens for data changes through the state decorator.
965
966```ts
967// src/main/ets/viewmodel/TaskViewModel.ets
968
969import TaskModel from '../model/TaskModel';
970
971@ObservedV2
972export default class TaskViewModel {
973  @Trace taskName: string = 'Todo';
974  @Trace isFinish: boolean = false;
975
976  updateTask(task: TaskModel) {
977    this.taskName = task.taskName;
978    this.isFinish = task.isFinish;
979  }
980
981  updateIsFinish(): void {
982    this.isFinish = !this.isFinish;
983  }
984}
985```
986
987- **TaskListViewModel**: encapsulates the task list and management functions, including loading tasks, updating task status in batches, and adding and deleting tasks.
988
989```ts
990// src/main/ets/viewmodel/TaskListViewModel.ets
991
992import { common } from '@kit.AbilityKit';
993import { Type } from '@kit.ArkUI';
994import TaskListModel from '../model/TaskListModel';
995import TaskViewModel from'./TaskViewModel';
996
997@ObservedV2
998export default class TaskListViewModel {
999  @Type(TaskViewModel)
1000  @Trace tasks: TaskViewModel[] = [];
1001
1002  async loadTasks(context: common.UIAbilityContext) {
1003    let taskList = new TaskListModel([]);
1004    await taskList.loadTasks(context);
1005    for(let task of taskList.tasks){
1006      let taskViewModel = new TaskViewModel();
1007      taskViewModel.updateTask(task);
1008      this.tasks.push(taskViewModel);
1009    }
1010  }
1011
1012  finishAll(ifFinish: boolean): void {
1013    for(let task of this.tasks){
1014      task.isFinish = ifFinish;
1015    }
1016  }
1017
1018  addTask(newTask: TaskViewModel): void {
1019    this.tasks.push(newTask);
1020  }
1021
1022  removeTask(removedTask: TaskViewModel): void {
1023    this.tasks.splice(this.tasks.indexOf(removedTask), 1)
1024  }
1025}
1026```
1027
1028### View
1029
1030The 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.
1031
1032- **TitleView**: displays application titles and statistics about unfinished tasks.
1033
1034```ts
1035// src/main/ets/view/TitleView.ets
1036
1037@ComponentV2
1038export default struct TitleView {
1039  @Param tasksUnfinished: number = 0;
1040
1041  build() {
1042    Column() {
1043      Text('To-Dos')
1044        .fontSize(40)
1045        .margin(10)
1046      Text('Unfinished: ${this.tasksUnfinished}')
1047        .margin({ left: 10, bottom: 10 })
1048    }
1049  }
1050}
1051```
1052
1053- **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.
1054
1055```ts
1056// src/main/ets/view/ListView.ets
1057
1058import TaskViewModel from '../viewmodel/TaskViewModel';
1059import TaskListViewModel from '../viewmodel/TaskListViewModel';
1060import { Setting } from '../pages/SettingPage';
1061import { ActionButton } from './BottomView';
1062
1063@ComponentV2
1064struct TaskItem {
1065  @Param task: TaskViewModel = new TaskViewModel();
1066  @Event deleteTask: () => void = () => {};
1067  @Monitor('task.isFinish')
1068  onTaskFinished(mon: IMonitor) {
1069    console.log('The status of' + this.task.taskName + 'has changed from' + mon.value()?.before + 'to' + mon.value()?.now);
1070  }
1071
1072  build() {
1073    Row() {
1074      // 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.
1075      Image(this.task.isFinish ? $r('app.media.finished') : $r('app.media.unfinished'))
1076        .width(28)
1077        .height(28)
1078        .margin({ left: 15, right: 10 })
1079      Text(this.task.taskName)
1080        .decoration({ type: this.task.isFinish ? TextDecorationType.LineThrough : TextDecorationType.None })
1081        .fontSize(18)
1082      ActionButton('Delete', () => this.deleteTask());
1083    }
1084    .height('7%')
1085    .width('90%')
1086    .backgroundColor('#90f1f3f5')
1087    .borderRadius(25)
1088    .onClick(() => this.task.updateIsFinish())
1089  }
1090}
1091
1092@ComponentV2
1093export default struct ListView {
1094  @Param taskList: TaskListViewModel = new TaskListViewModel();
1095  @Param setting: Setting = new Setting();
1096
1097  build() {
1098    Repeat<TaskViewModel>(this.taskList.tasks.filter(task => this.setting.showCompletedTask || !task.isFinish))
1099      .each((obj: RepeatItem<TaskViewModel>) => {
1100        TaskItem({
1101          task: obj.item,
1102          deleteTask: () => this.taskList.removeTask(obj.item)
1103        }).margin(5)
1104      })
1105  }
1106}
1107```
1108
1109- **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.
1110
1111```ts
1112// src/main/ets/view/BottomView.ets
1113
1114import { common, Want } from '@kit.AbilityKit';
1115import TaskViewModel from '../viewmodel/TaskViewModel';
1116import TaskListViewModel from '../viewmodel/TaskListViewModel';
1117
1118@Builder export function ActionButton(text: string, onClick:() => void) {
1119  Button(text, { buttonStyle: ButtonStyleMode.NORMAL })
1120    .onClick(onClick)
1121    .margin({ left: 10, right: 10, top: 5, bottom: 5 })
1122}
1123
1124@ComponentV2
1125export default struct BottomView {
1126  @Param taskList: TaskListViewModel = new TaskListViewModel();
1127  @Local newTaskName: string = '';
1128  private context = getContext() as common.UIAbilityContext;
1129
1130  build() {
1131    Column() {
1132      Row() {
1133        ActionButton('All Finished', (): void => this.taskList.finishAll(true))
1134        ActionButton('All Unfinished', (): void => this.taskList.finishAll(false))
1135        ActionButton('Settings', (): void => {
1136          let wantInfo: Want = {
1137            deviceId: '', // An empty deviceId indicates the local device.
1138            bundleName: 'com.samples.statemgmtv2mvvm', // Replace it with the bundle name in AppScope/app.json5.
1139            abilityName: 'SettingAbility',
1140          };
1141          this.context.startAbility(wantInfo);
1142        })
1143      }
1144      .margin({ top: 10, bottom: 5 })
1145      Row() {
1146        TextInput({ placeholder: 'Add a new task', text: this.newTaskName })
1147          .onChange((value) => this.newTaskName = value)
1148          .width('70%')
1149        ActionButton('+', (): void => {
1150          let newTask = new TaskViewModel();
1151          newTask.taskName = this.newTaskName;
1152          this.taskList.addTask(newTask);
1153          this.newTaskName = '';
1154        })
1155      }
1156    }
1157  }
1158}
1159```
1160
1161- **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.
1162
1163```ts
1164// src/main/ets/pages/TodoListPage.ets
1165
1166import TaskListViewModel from '../viewmodel/TaskListViewModel';
1167import { common } from '@kit.AbilityKit';
1168import { AppStorageV2, PersistenceV2 } from '@kit.ArkUI';
1169import { Setting } from '../pages/SettingPage';
1170import TitleView from '../view/TitleView';
1171import ListView from '../view/ListView';
1172import BottomView from '../view/BottomView';
1173
1174@Entry
1175@ComponentV2
1176struct TodoList {
1177  @Local taskList: TaskListViewModel = PersistenceV2.connect(TaskListViewModel, 'TaskList', () => new TaskListViewModel())!;
1178  @Local setting: Setting = AppStorageV2.connect(Setting, 'Setting', () => new Setting())!;
1179  private context = getContext(this) as common.UIAbilityContext;
1180
1181  async aboutToAppear() {
1182    this.taskList = PersistenceV2.connect(TaskListViewModel, 'TaskList', () => new TaskListViewModel())!;
1183    if (this.taskList.tasks.length === 0) {
1184      await this.taskList.loadTasks(this.context);
1185    }
1186  }
1187
1188  @Computed
1189  get tasksUnfinished(): number {
1190    return this.taskList.tasks.filter(task => !task.isFinish).length;
1191  }
1192
1193  build() {
1194    Column() {
1195      TitleView({ tasksUnfinished: this.tasksUnfinished })
1196      ListView({ taskList: this.taskList, setting: this.setting });
1197      BottomView({ taskList: this.taskList });
1198    }
1199    .height('100%')
1200    .width('100%')
1201    .alignItems(HorizontalAlign.Start)
1202    .margin({ left: 15 })
1203  }
1204}
1205```
1206
1207- **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.
1208
1209```ts
1210// src/main/ets/pages/SettingPage.ets
1211
1212import { AppStorageV2 } from '@kit.ArkUI';
1213import { common } from '@kit.AbilityKit';
1214
1215@ObservedV2
1216export class Setting {
1217  @Trace showCompletedTask: boolean = true;
1218}
1219
1220@Entry
1221@ComponentV2
1222struct SettingPage {
1223  @Local setting: Setting = AppStorageV2.connect(Setting, 'Setting', () => new Setting())!;
1224  private context = getContext(this) as common.UIAbilityContext;
1225
1226  build(){
1227    Column(){
1228      Text('Settings')
1229        .fontSize(40)
1230        .margin({ bottom: 10 })
1231      Row() {
1232        Text('Show finished');
1233        Toggle({ type: ToggleType.Switch, isOn:this.setting.showCompletedTask })
1234          .onChange((isOn) => {
1235            this.setting.showCompletedTask = isOn;
1236          })
1237      }
1238      Button('Back')
1239        .onClick(()=>this.context.terminateSelf())
1240        .margin({ top: 10 })
1241    }
1242    .alignItems(HorizontalAlign.Start)
1243  }
1244}
1245```
1246
1247## Summary
1248
1249This 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.
1250
1251## Sample Code
1252[Complete Source Code](https://gitee.com/openharmony/applications_app_samples/tree/master/code/DocsSample/ArkUISample/StateMgmtV2MVVM/entry)
1253