1# MVVM模式 2<!--Kit: ArkUI--> 3<!--Subsystem: ArkUI--> 4<!--Owner: @zzq212050299--> 5<!--Designer: @s10021109--> 6<!--Tester: @TerryTsao--> 7<!--Adviser: @zhang_yixin13--> 8 9当开发者掌握了状态管理的基本概念后,通常会尝试开发自己的应用,在应用开发初期,如果未能精心规划项目结构,随着项目扩展和复杂化,状态变量的增多将导致组件间关系变得错综复杂。此时,开发新功能可能引起连锁反应,维护成本也会增加。为此,本文旨在介绍MVVM模式以及ArkUI的UI开发模式与MVVM的关系,指导开发者如何设计项目结构,以便在产品迭代和升级时,能更轻松的开发和维护。 10 11 12本文档涵盖了大多数状态管理V1装饰器,所以在阅读本文档前,建议开发者对状态管理V1有一定的了解。建议提前阅读:[状态管理概述](./arkts-state-management-overview.md)和状态管理V1装饰器相关文档。 13 14## MVVM模式介绍 15 16### 概念 17 18在应用开发中,UI更新需要实时同步数据状态变化,这直接影响应用程序的性能和用户体验。为了解决数据与UI同步的复杂性,ArkUI采用了 Model-View-ViewModel(MVVM)架构模式。MVVM 将应用分为Model、View和ViewModel三个核心部分,实现数据、视图与逻辑的分离。通过这种模式,UI可以自动更新状态变化,从而更高效地管理数据和视图的绑定与更新。 19 20- View:用户界面层。负责用户界面展示并与用户交互,不包含任何业务逻辑。它通过绑定ViewModel层提供的数据实现动态更新。 21- Model:数据访问层。以数据为中心,不直接与用户界面交互。负责数据结构定义,数据管理(获取、存储、更新等),以及业务逻辑处理。 22- ViewModel:表示逻辑层。作为连接Model和View的桥梁,通常一个View对应一个ViewModel。View和ViewModel有两种通信方式: 23 1.方法调用:View通过事件监听用户行为,在回调里面触发ViewModel层的方法。例如当View监听到用户Button点击行为,调用ViewModel对应的方法,处理用户操作。 24 2.双向绑定:View绑定ViewModel的数据,实现双向同步。 25 26ArkUI的UI开发模式就属于MVVM模式,通过对MVVM概念的基本介绍,开发者大致能猜到状态管理能在MVVM中起什么样的作用,状态管理旨在数据驱动更新,让开发者只用关注页面设计,而不去关注整个UI的刷新逻辑,数据的维护也无需开发者进行感知,由状态变量自动更新完成,而这就是属于ViewModel层所需要支持的内容,因此开发者使用MVVM模式开发自己的应用是最省心省力的。 27 28### ArkUI开发模式图 29 30ArkUI的UI开发模式即是MVVM模式,而状态变量在MVVM模式中扮演着ViewModel的角色,向上刷新UI,向下更新数据,整体框架如下图: 31 32 33 34### 分层说明 35 36**View层** 37 38View层通常可以分为下列组件: 39* 页面组件:所有应用基本都是按照页面进行分类的,比如登录页,列表页,编辑页,帮助页,版权页等。每个页面对应需要的数据可能是完全不一样的,也可能多个页面需要的数据是同一套。 40* 业务组件:本身具备本APP部分业务能力的功能组件,典型的就是这个业务组件可能关联了本项目的ViewModel中的数据,不可以被共享给其他项目使用。 41* 通用组件:像系统组件一样,这类组件不会关联本APP中ViewModel的数据,这些组件可实现跨越多个项目进行共享,来完成比较通用的功能。 42 43**Model层** 44 45Model层是应用的原始数据提供者,代表应用的核心业务逻辑和数据。 46 47**ViewModel层** 48 49为View层的组件提供对应数据,按照页面组织,当用户浏览页面时,某些页面可能不会被显示,因此,页面数据最好设计成懒加载(按需加载)的模式。 50 51> ViewModel层数据和Model层数据的区别: 52> 53> Model层数据是按照整个工程、项目来组织数据,构成一套完整的APP业务数据体系。 54> 55> ViewModel层数据,是提供某个页面上使用的数据,它可能是整个APP的业务数据的一部分。另外ViewModel层还可以附加对应Page的辅助页面显示数据,这部分数据可能与本APP的业务完全无关,仅仅是为页面展示提供便利的辅助数据。 56 57### 架构核心原则 58 59**不可跨层访问** 60 61* View层不可以直接调用Model层的数据,只能通过ViewModel提供的方法进行调用。 62* Model层数据,不可以直接操作UI,Model层只能通知ViewModel层数据有更新,由ViewModel层更新对应的数据。 63 64**下层不可访问上层数据** 65 66下层数据通过通知模式更新上层数据。在业务逻辑中,下层不可直接获取上层数据。例如,ViewModel层的逻辑处理不应该依赖View层界面上的某个值。 67 68**非父子组件间不可直接访问** 69 70这是针对View层设计的核心原则,一个组件应该具备以下逻辑: 71 72* 禁止直接访问父组件(必须使用事件或是订阅能力)。 73* 禁止直接访问兄弟组件。这是因为组件应该仅能访问自己的子节点(通过传参)和父节点(通过事件或通知),以此完成组件之间的解耦。 74 75对于一个组件,这样设计的原因如下: 76 77* 组件自己使用了哪些子组件是明确的,因此可以访问。 78* 组件被放置于哪个父节点下是未知的,因此组件想访问父节点,就只能通过通知或者事件能力完成。 79* 组件不可能知道自己的兄弟节点是谁,因此组件不可以操纵兄弟节点。 80 81## 备忘录开发实战 82 83本节通过备忘录应用的开发,使开发者了解如何使用ArkUI框架设计自己的应用。本节直接进行功能开发,未设计代码架构,即根据需求即时开发,不考虑后续维护,同时,本节还将介绍功能开发所需的装饰器。 84 85### @State状态变量 86 87* @State装饰器是最常用的装饰器之一,用于定义状态变量。通常,这些状态变量作为父组件的数据源,开发者点击时,触发状态变量的更新,刷新UI。 88 89```typescript 90@Entry 91@Component 92struct Index { 93 @State isFinished: boolean = false; 94 95 build() { 96 Column() { 97 Row() { 98 Text('全部待办') 99 .fontSize(30) 100 .fontWeight(FontWeight.Bold) 101 } 102 .width('100%') 103 .margin({top: 10, bottom: 10}) 104 105 // 待办事项 106 Row({space: 15}) { 107 if (this.isFinished) { 108 // 此处'app.media.finished'仅作示例,请开发者自行替换,否则imageSource创建失败会导致后续无法正常执行。 109 Image($r('app.media.finished')) 110 .width(28) 111 .height(28) 112 } 113 else { 114 // 此处'app.media.unfinished'仅作示例,请开发者自行替换,否则imageSource创建失败会导致后续无法正常执行。 115 Image($r('app.media.unfinished')) 116 .width(28) 117 .height(28) 118 } 119 Text('学习高数') 120 .fontSize(24) 121 .decoration({type: this.isFinished ? TextDecorationType.LineThrough : TextDecorationType.None}) 122 } 123 .height('40%') 124 .width('100%') 125 .border({width: 5}) 126 .padding({left: 15}) 127 .onClick(() => { 128 this.isFinished = !this.isFinished; 129 }) 130 } 131 .height('100%') 132 .width('100%') 133 .margin({top: 5, bottom: 5}) 134 .backgroundColor('#90f1f3f5') 135 } 136} 137``` 138 139效果图: 140 141 142 143### @Prop、@Link的作用 144 145上述示例中,所有代码都写在了`@Entry`组件中。随着需要渲染的组件越来越多,`@Entry`组件必然需要进行拆分,为此,拆分出的子组件就需要使用\@Prop和\@Link装饰器: 146 147* \@Prop是父子间单向传递,子组件会深拷贝父组件数据,可从父组件更新,也可自己更新数据,但不会同步回父组件。 148* \@Link是父子间双向传递,父组件改变,会通知所有的\@Link,同时\@Link的更新也会通知父组件的数据源进行刷新。 149 150```typescript 151@Component 152struct TodoComponent { 153 build() { 154 Row() { 155 Text('全部待办') 156 .fontSize(30) 157 .fontWeight(FontWeight.Bold) 158 } 159 .width('100%') 160 .margin({top: 10, bottom: 10}) 161 } 162} 163 164@Component 165struct AllChooseComponent { 166 @Link isFinished: boolean; 167 168 build() { 169 Row() { 170 Button('全选', {type: ButtonType.Normal}) 171 .onClick(() => { 172 this.isFinished = !this.isFinished; 173 }) 174 .fontSize(30) 175 .fontWeight(FontWeight.Bold) 176 .backgroundColor('#f7f6cc74') 177 } 178 .padding({left: 15}) 179 .width('100%') 180 .margin({top: 10, bottom: 10}) 181 } 182} 183 184@Component 185struct ThingsComponent1 { 186 @Prop isFinished: boolean; 187 188 build() { 189 // 待办事项1 190 Row({space: 15}) { 191 if (this.isFinished) { 192 // 此处'app.media.finished'仅作示例,请开发者自行替换,否则imageSource创建失败会导致后续无法正常执行。 193 Image($r('app.media.finished')) 194 .width(28) 195 .height(28) 196 } 197 else { 198 // 此处'app.media.unfinished'仅作示例,请开发者自行替换,否则imageSource创建失败会导致后续无法正常执行。 199 Image($r('app.media.unfinished')) 200 .width(28) 201 .height(28) 202 } 203 Text('学习语文') 204 .fontSize(24) 205 .decoration({type: this.isFinished ? TextDecorationType.LineThrough : TextDecorationType.None}) 206 } 207 .height('40%') 208 .width('100%') 209 .border({width: 5}) 210 .padding({left: 15}) 211 .onClick(() => { 212 this.isFinished = !this.isFinished; 213 }) 214 } 215} 216 217@Component 218struct ThingsComponent2 { 219 @Prop isFinished: boolean; 220 221 build() { 222 // 待办事项1 223 Row({space: 15}) { 224 if (this.isFinished) { 225 // 此处'app.media.finished'仅作示例,请开发者自行替换,否则imageSource创建失败会导致后续无法正常执行。 226 Image($r('app.media.finished')) 227 .width(28) 228 .height(28) 229 } 230 else { 231 // 此处'app.media.unfinished'仅作示例,请开发者自行替换,否则imageSource创建失败会导致后续无法正常执行。 232 Image($r('app.media.unfinished')) 233 .width(28) 234 .height(28) 235 } 236 Text('学习高数') 237 .fontSize(24) 238 .decoration({type: this.isFinished ? TextDecorationType.LineThrough : TextDecorationType.None}) 239 } 240 .height('40%') 241 .width('100%') 242 .border({width: 5}) 243 .padding({left: 15}) 244 .onClick(() => { 245 this.isFinished = !this.isFinished; 246 }) 247 } 248} 249 250@Entry 251@Component 252struct Index { 253 @State isFinished: boolean = false; 254 255 build() { 256 Column() { 257 // 全部待办 258 TodoComponent() 259 260 // 全选 261 AllChooseComponent({isFinished: this.isFinished}) 262 263 // 待办事项1 264 ThingsComponent1({isFinished: this.isFinished}) 265 266 // 待办事项2 267 ThingsComponent2({isFinished: this.isFinished}) 268 } 269 .height('100%') 270 .width('100%') 271 .margin({top: 5, bottom: 5}) 272 .backgroundColor('#90f1f3f5') 273 } 274} 275``` 276 277效果图如下: 278 279 280 281### 循环渲染组件 282 283* 上个示例虽然拆分出了子组件,但发现组件1和组件2的代码非常相似,当渲染的组件除了数据外,其他设置都相同时,此时就需要使用ForEach循环渲染。 284* ForEach使用之后,冗余代码变得更少,并且代码结构更加清晰。 285 286```typescript 287@Component 288struct TodoComponent { 289 build() { 290 Row() { 291 Text('全部待办') 292 .fontSize(30) 293 .fontWeight(FontWeight.Bold) 294 } 295 .width('100%') 296 .margin({top: 10, bottom: 10}) 297 } 298} 299 300@Component 301struct AllChooseComponent { 302 @Link isFinished: boolean; 303 304 build() { 305 Row() { 306 Button('全选', {type: ButtonType.Normal}) 307 .onClick(() => { 308 this.isFinished = !this.isFinished; 309 }) 310 .fontSize(30) 311 .fontWeight(FontWeight.Bold) 312 .backgroundColor('#f7f6cc74') 313 } 314 .padding({left: 15}) 315 .width('100%') 316 .margin({top: 10, bottom: 10}) 317 } 318} 319 320@Component 321struct ThingsComponent { 322 @Prop isFinished: boolean; 323 @Prop things: string; 324 build() { 325 // 待办事项1 326 Row({space: 15}) { 327 if (this.isFinished) { 328 // 此处'app.media.finished'仅作示例,请开发者自行替换,否则imageSource创建失败会导致后续无法正常执行。 329 Image($r('app.media.finished')) 330 .width(28) 331 .height(28) 332 } 333 else { 334 // 此处'app.media.unfinished'仅作示例,请开发者自行替换,否则imageSource创建失败会导致后续无法正常执行。 335 Image($r('app.media.unfinished')) 336 .width(28) 337 .height(28) 338 } 339 Text(`${this.things}`) 340 .fontSize(24) 341 .decoration({type: this.isFinished ? TextDecorationType.LineThrough : TextDecorationType.None}) 342 } 343 .height('8%') 344 .width('90%') 345 .padding({left: 15}) 346 .opacity(this.isFinished ? 0.3: 1) 347 .border({width:1}) 348 .borderColor(Color.White) 349 .borderRadius(25) 350 .backgroundColor(Color.White) 351 .onClick(() => { 352 this.isFinished = !this.isFinished; 353 }) 354 } 355} 356 357@Entry 358@Component 359struct Index { 360 @State isFinished: boolean = false; 361 @State planList: string[] = [ 362 '7.30 起床', 363 '8.30 早餐', 364 '11.30 中餐', 365 '17.30 晚餐', 366 '21.30 夜宵', 367 '22.30 洗澡', 368 '1.30 起床' 369 ]; 370 371 build() { 372 Column() { 373 // 全部待办 374 TodoComponent() 375 376 // 全选 377 AllChooseComponent({isFinished: this.isFinished}) 378 379 List() { 380 ForEach(this.planList, (item: string) => { 381 // 待办事项1 382 ThingsComponent({isFinished: this.isFinished, things: item}) 383 .margin(5) 384 }) 385 } 386 387 } 388 .height('100%') 389 .width('100%') 390 .margin({top: 5, bottom: 5}) 391 .backgroundColor('#90f1f3f5') 392 } 393} 394``` 395 396效果图如下: 397 398 399 400### @Builder方法 401 402* Builder方法用于组件内定义方法,可以使得相同代码可以在组件内进行复用。 403* 本示例不仅使用了@Builder方法进行去重,还对数据进行了移除,可以看到此时代码更加清晰易读,相对于最开始的代码,`@Entry`组件基本只用于处理页面构建逻辑,而不处理大量与页面设计无关的内容。 404 405```typescript 406@Observed 407class TodoListData { 408 planList: string[] = [ 409 '7.30 起床', 410 '8.30 早餐', 411 '11.30 中餐', 412 '17.30 晚餐', 413 '21.30 夜宵', 414 '22.30 洗澡', 415 '1.30 起床' 416 ]; 417} 418 419@Component 420struct TodoComponent { 421 build() { 422 Row() { 423 Text('全部待办') 424 .fontSize(30) 425 .fontWeight(FontWeight.Bold) 426 } 427 .width('100%') 428 .margin({top: 10, bottom: 10}) 429 } 430} 431 432@Component 433struct AllChooseComponent { 434 @Link isFinished: boolean; 435 436 build() { 437 Row() { 438 Button('全选', {type: ButtonType.Capsule}) 439 .onClick(() => { 440 this.isFinished = !this.isFinished; 441 }) 442 .fontSize(30) 443 .fontWeight(FontWeight.Bold) 444 .backgroundColor('#f7f6cc74') 445 } 446 .padding({left: 15}) 447 .width('100%') 448 .margin({top: 10, bottom: 10}) 449 } 450} 451 452@Component 453struct ThingsComponent { 454 @Prop isFinished: boolean; 455 @Prop things: string; 456 457 @Builder displayIcon(icon: Resource) { 458 Image(icon) 459 .width(28) 460 .height(28) 461 .onClick(() => { 462 this.isFinished = !this.isFinished; 463 }) 464 } 465 466 build() { 467 // 待办事项1 468 Row({space: 15}) { 469 if (this.isFinished) { 470 // 此处'app.media.finished'仅作示例,请开发者自行替换,否则imageSource创建失败会导致后续无法正常执行。 471 this.displayIcon($r('app.media.finished')); 472 } 473 else { 474 // 此处'app.media.unfinished'仅作示例,请开发者自行替换,否则imageSource创建失败会导致后续无法正常执行。 475 this.displayIcon($r('app.media.unfinished')); 476 } 477 Text(`${this.things}`) 478 .fontSize(24) 479 .decoration({type: this.isFinished ? TextDecorationType.LineThrough : TextDecorationType.None}) 480 .onClick(() => { 481 this.things += '啦'; 482 }) 483 } 484 .height('8%') 485 .width('90%') 486 .padding({left: 15}) 487 .opacity(this.isFinished ? 0.3: 1) 488 .border({width:1}) 489 .borderColor(Color.White) 490 .borderRadius(25) 491 .backgroundColor(Color.White) 492 } 493} 494 495@Entry 496@Component 497struct Index { 498 @State isFinished: boolean = false; 499 @State data: TodoListData = new TodoListData(); // View绑定ViewModel的数据 500 501 build() { 502 Column() { 503 // 全部待办 504 TodoComponent() 505 506 // 全选 507 AllChooseComponent({isFinished: this.isFinished}) 508 509 List() { 510 ForEach(this.data.planList, (item: string) => { 511 // 待办事项1 512 ThingsComponent({isFinished: this.isFinished, things: item}) 513 .margin(5) 514 }) 515 } 516 517 } 518 .height('100%') 519 .width('100%') 520 .margin({top: 5, bottom: 5}) 521 .backgroundColor('#90f1f3f5') 522 } 523} 524``` 525 526 效果图如下: 527 528 529 530### 总结 531 532* 通过逐步优化代码结构,可以看到`\@Entry`组件作为页面的入口,其build函数应该仅考虑将需要的组件组合起来,类似于搭积木。被page调用的子组件则如同积木,等着被需要的page进行调用。状态变量类似于粘合剂,当触发UI刷新事件时,状态变量自动刷新绑定的组件,实现page的按需刷新。 533* 虽然现有的架构并未使用到MVVM的设计理念,但MVVM的核心理念已初见端倪。ArkUI的UI开发天然适合MVVM模式。在ArkUI中,page和组件构成View层,page负责组织组件,组件则作为构成元素。当组件需要更新时,通过状态变量驱动组件刷新,从而更新page。ViewModel的数据则来源于Model层。 534* 示例中的代码功能较为简单,但随着功能的增加,主页面的代码量也会逐渐增多。当备忘录需要添加更多功能,且其他页面也需要使用到主页面的组件时,可以考虑采用MVVM模式来组织项目结构。 535 536## 通过MVVM开发备忘录实战 537 538上一章节展示了非MVVM模式下的代码组织方式。随着主页面代码的增加,应该采取合理的分层策略,使项目结构清晰,组件之间不互相引用,避免后期维护时牵一发而动全身,增加功能更新的困难。本章将通过对MVVM的核心文件组织模式,向开发者展示如何使用MVVM来重构上一章节的代码。 539 540### MVVM文件结构说明 541 542* src 543 * ets 544 * pages ------ 存放页面组件 545 * views ------ 存放业务组件 546 * shares ------ 存放通用组件 547 * viewModel ------ 数据服务 548 * LoginViewModel ----- 登录页ViewModel 549 * xxxViewModel ------ 其他页ViewModel 550 551### 分层设计技巧 552 553**Model层** 554 555* model层存放本应用核心数据结构,这层本身和UI开发关系不大,让用户按照自己的业务逻辑进行封装。 556 557**ViewModel层** 558 559> 注意: 560> 561> ViewModel层不只是存放数据,它同时需要提供数据的服务及处理。 562 563* ViewModel层是为视图服务的数据层。其设计具有两个特点: 564 1. 按照页面组织数据。 565 2. 每个页面数据进行懒加载。 566 567**View层** 568 569View层根据需要来组织,但View层需要区分一下三种组件: 570 571* 页面组件:提供整体页面布局,实现多页面之间的跳转,前后台事件处理等页面内容。 572* 业务组件:被页面引用,构建出页面。 573* 共享组件:与项目无关的多项目共享组件。 574 575> 共享组件和业务组件的区别: 576> 577> 业务组件包含了ViewModel数据,没有ViewModel,这个组件不能运行。 578> 579> 共享组件:不包含ViewModel层的数据,需要的数据从外部传入。共享组件包含一个自定义组件,只要外部参数(无业务参数)满足,就可以工作。 580 581### 代码示例 582 583按MVVM模式组织结构,重构如下: 584 585* src 586 * ets 587 * model 588 * ThingModel 589 * TodoListModel 590 * pages 591 * Index 592 * view 593 * AllChooseComponent 594 * ThingsComponent 595 * TodoComponent 596 * TodoListComponent 597 * viewModel 598 * ThingsViewModel 599 * TodoListViewModel 600 * resources 601 * rawfile 602 * default_tasks.json 603 604文件代码如下: 605 606* Index.ets 607 608 ```typescript 609 import { common } from '@kit.AbilityKit'; 610 // import ViewModel 611 import TodoListViewModel from '../ViewModel/TodoListViewModel'; 612 613 // import View 614 import { TodoComponent } from '../View/TodoComponent'; 615 import { AllChooseComponent } from '../View/AllChooseComponent'; 616 import { TodoListComponent } from '../View/TodoListComponent'; 617 618 @Entry 619 @Component 620 struct TodoList { 621 @State todoListViewModel: TodoListViewModel = new TodoListViewModel(); // View绑定ViewModel的数据 622 private context = this.getUIContext().getHostContext() as common.UIAbilityContext; 623 624 async aboutToAppear() { 625 await this.todoListViewModel.loadTasks(this.context); 626 } 627 628 build() { 629 Column() { 630 Row({ space: 40 }) { 631 // 全部待办 632 TodoComponent() 633 // 全选 634 AllChooseComponent({ todoListViewModel: this.todoListViewModel }) 635 } 636 637 Column() { 638 TodoListComponent({ thingsViewModelArray: this.todoListViewModel.things }) 639 } 640 } 641 .height('100%') 642 .width('100%') 643 .margin({ top: 5, bottom: 5 }) 644 .backgroundColor('#90f1f3f5') 645 } 646 } 647 ``` 648 649 * ThingModel.ets 650 651 ```typescript 652 export default class ThingModel { 653 thingsName: string = 'Todo'; 654 isFinish: boolean = false; 655 } 656 ``` 657 658 * TodoListModel.ets 659 660 ```typescript 661 import { common } from '@kit.AbilityKit'; 662 import { util } from '@kit.ArkTs'; 663 import ThingModel from './ThingModel'; 664 665 export default class TodoListModel { 666 things: Array<ThingModel> = []; 667 668 constructor(things: Array<ThingModel>) { 669 this.things = things; 670 } 671 672 async loadTasks(context: common.UIAbilityContext) { 673 let getJson = await context.resourceManager.getRawFileContent('default_tasks.json'); 674 let textDecoderOptions: util.TextDecoderOptions = { ignoreBOM: true }; 675 let textDecoder = util.TextDecoder.create('utf-8', textDecoderOptions); 676 let result = textDecoder.decodeToString(getJson, { stream: false }); 677 this.things = JSON.parse(result); 678 } 679 } 680 ``` 681 682 * AllChooseComponent.ets 683 684 ```typescript 685 import TodoListViewModel from "../ViewModel/TodoListViewModel"; 686 687 @Component 688 export struct AllChooseComponent { 689 @State titleName: string = '全选'; 690 @Link todoListViewModel: TodoListViewModel; 691 692 build() { 693 Row() { 694 Button(`${this.titleName}`, { type: ButtonType.Capsule }) 695 .onClick(() => { 696 this.todoListViewModel.chooseAll(); // View层点击事件发生时,调用ViewModel层方法chooseAll处理逻辑 697 this.titleName = this.todoListViewModel.isChoosen ? '全选' : '取消全选'; 698 }) 699 .fontSize(30) 700 .fontWeight(FontWeight.Bold) 701 .backgroundColor('#f7f6cc74') 702 } 703 .padding({ left: this.todoListViewModel.isChoosen ? 15 : 0 }) 704 .width('100%') 705 .margin({ top: 10, bottom: 10 }) 706 } 707 } 708 ``` 709 710 * ThingsComponent.ets 711 712 ```typescript 713 import ThingsViewModel from "../ViewModel/ThingsViewModel"; 714 715 @Component 716 export struct ThingsComponent { 717 @Prop things: ThingsViewModel; 718 719 @Builder 720 displayIcon(icon: Resource) { 721 Image(icon) 722 .width(28) 723 .height(28) 724 .onClick(() => { 725 this.things.updateIsFinish(); // View层点击事件发生时,调用ViewModel层方法updateIsFinish处理逻辑 726 }) 727 } 728 729 build() { 730 // 待办事项 731 Row({ space: 15 }) { 732 if(this.things.isFinish) { 733 // 此处'app.media.finished'仅作示例,请开发者自行替换,否则imageSource创建失败会导致后续无法正常执行。 734 this.displayIcon($r('app.media.finished')); 735 } else { 736 // 此处'app.media.unfinished'仅作示例,请开发者自行替换,否则imageSource创建失败会导致后续无法正常执行。 737 this.displayIcon($r('app.media.unfinished')); 738 } 739 740 Text(`${this.things.thingsName}`) 741 .fontSize(24) 742 .decoration({ type: this.things.isFinish ? TextDecorationType.LineThrough: TextDecorationType.None }) 743 .onClick(() => { 744 this.things.addSuffixes(); // View层点击事件发生时,调用ViewModel层方法addSuffixes处理逻辑 745 }) 746 } 747 .height('8%') 748 .width('90%') 749 .padding({ left: 15 }) 750 .opacity(this.things.isFinish ? 0.3 : 1) 751 .border({ width: 1 }) 752 .borderColor(Color.White) 753 .borderRadius(25) 754 .backgroundColor(Color.White) 755 } 756 } 757 ``` 758 759 * TodoComponent.ets 760 761 ```typescript 762 @Component 763 export struct TodoComponent { 764 build() { 765 Row() { 766 Text('全部待办') 767 .fontSize(30) 768 .fontWeight(FontWeight.Bold) 769 } 770 .padding({ left: 15 }) 771 .width('50%') 772 .margin({ top: 10, bottom: 10 }) 773 } 774 } 775 ``` 776 777 * TodoListComponent.ets 778 779 ```typescript 780 import ThingsViewModel from "../ViewModel/ThingsViewModel"; 781 import { ThingsViewModelArray } from "../ViewModel/TodoListViewModel" 782 import { ThingsComponent } from "./ThingsComponent"; 783 784 @Component 785 export struct TodoListComponent { 786 @ObjectLink thingsViewModelArray: ThingsViewModelArray; 787 788 build() { 789 Column() { 790 List() { 791 ForEach(this.thingsViewModelArray, (item: ThingsViewModel) => { 792 // 待办事项 793 ListItem() { 794 ThingsComponent({ things: item }) 795 .margin(5) 796 } 797 }, (item: ThingsViewModel) => { 798 return item.thingsName; 799 }) 800 } 801 } 802 } 803 } 804 ``` 805 806 * ThingsViewModel.ets 807 808 ```typescript 809 import ThingModel from "../Model/ThingModel"; 810 811 @Observed 812 export default class ThingsViewModel { 813 @Track thingsName: string = 'Todo'; 814 @Track isFinish: boolean = false; 815 816 updateTask(things: ThingModel) { 817 this.thingsName = things.thingsName; 818 this.isFinish = things.isFinish; 819 } 820 821 updateIsFinish(): void { 822 this.isFinish = !this.isFinish; 823 } 824 825 addSuffixes(): void { 826 this.thingsName += '啦'; 827 } 828 } 829 ``` 830 831 * TodoListViewModel.ets 832 833 ```typescript 834 import ThingsViewModel from "./ThingsViewModel"; 835 import { common } from "@kit.AbilityKit"; 836 import TodoListModel from "../Model/TodoListModel"; 837 838 @Observed 839 export class ThingsViewModelArray extends Array<ThingsViewModel> { 840 } 841 842 @Observed 843 export default class TodoListViewModel { 844 @Track isChoosen: boolean = true; 845 @Track things: ThingsViewModelArray = new ThingsViewModelArray(); 846 847 async loadTasks(context: common.UIAbilityContext) { 848 let todoList = new TodoListModel([]); 849 await todoList.loadTasks(context); 850 for(let things of todoList.things) { 851 let todoListViewModel = new ThingsViewModel(); 852 todoListViewModel.updateTask(things); 853 this.things.push(todoListViewModel); 854 } 855 } 856 857 chooseAll(): void { 858 for(let things of this.things) { 859 things.isFinish = this.isChoosen; 860 } 861 this.isChoosen = !this.isChoosen; 862 } 863 } 864 ``` 865 866 * default_tasks.json 867 868 ```typescript 869 [ 870 {"thingsName": "7.30起床", "isFinish": false}, 871 {"thingsName": "8.30早餐", "isFinish": false}, 872 {"thingsName": "11.30中餐", "isFinish": false}, 873 {"thingsName": "17.30晚餐", "isFinish": false}, 874 {"thingsName": "21.30夜宵", "isFinish": false}, 875 {"thingsName": "22.30洗澡", "isFinish": false}, 876 {"thingsName": "1.30睡觉", "isFinish": false} 877 ] 878 ``` 879 880 MVVM模式拆分后的代码结构更加清晰,模块职责更明确。新页面需要使用事件组件,比如TodoListComponent组件,只需导入组件。 881 882 效果图如下: 883 884  885 886 887 888 889 890