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