• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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![MVVM图](./figures/MVVM_架构.png)
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![state](./figures/MVVM_state.gif)
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![Prop&Link](./figures/MVVM_Prop&Link.gif)
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![ForEach](./figures/MVVM_ForEach.gif)
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![builder](./figures/MVVM_builder.gif)
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  ![MVVM_index.gif](./figures/MVVM_index.gif)
883
884
885
886
887
888