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 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