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