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