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的桥梁,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层是应用的原始数据提供者,这一层在UI来看,有两种模式
47
48* 本地实现:通过纯NativeC++实现
49
50* 远端实现:通过IO端口(RestFul)实现
51
52> 注意:
53>
54> 采用本地实现时,系统的对数据加工和处理,基本上一定会存在非UI线程模型,这个时候,被加工的数据变更可能需要即时通知ViewModel层,来引起数据的变化,从而引起UI的相应更新。这个时候,自动线程转换就会变得非常重要。常规下,ViewModel层,View层,都只能在UI线程下执行,才能正常工作。因此需要一种机制,当需要通知UI更新时,需要自动完成线程切换。
55
56### 架构核心原则
57
58**不可跨层访问**
59
60* View层不可以直接调用Model层的数据,只能通过ViewModel提供的方法进行调用。
61* Model层数据,不可以直接操作UI,Model层只能通知ViewModel层数据有更新,由ViewModel层更新对应的数据。
62
63**下层不可访问上层数据**
64
65下层的数据通过通知模式更新上层数据。在业务逻辑中,下层不可直接写代码去获取上层数据。如ViewModel层的逻辑处理,不能去依赖View层界面上的某个值。
66
67**非父子组件间不可直接访问**
68
69这是针对View层设计的核心原则,一个组件应该具备这样的逻辑:
70
71* 禁止直接访问父组件(使用事件或是订阅能力)
72* 禁止直接访问兄弟组件能力。这是因为组件应该仅能访问自己看的见的子节点(通过传参)和父节点(通过事件或通知),以此完成组件之间的解耦。
73
74对于一个组件,这样设计的原因是:
75
76* 组件自己使用了哪些子组件是明确的,因此可以访问。
77* 组件被放置于哪个父节点下是未知的,因此组件想访问父节点,就只能通过通知或者事件能力完成。
78* 组件不可能知道自己的兄弟节点是谁,因此组件不可以操纵兄弟节点。
79
80## 备忘录开发实战
81
82本节通过备忘录应用的开发,让开发者了解如何通过ArkUI框架设计自己的应用,本节未设计代码架构直接进行功能开发,即根据需求做即时开发,不考虑后续维护,同时向开发者介绍功能开发所需的装饰器。
83
84### @State状态变量
85
86* @State装饰器作为最常用的装饰器,用来定义状态变量,一般作为父组件的数据源,当开发者点击时,通过触发状态变量的更新从而刷新UI,去掉@State则不再支持刷新UI。
87
88```typescript
89@Entry
90@Component
91struct Index {
92  @State isFinished: boolean = false;
93
94  build() {
95    Column() {
96      Row() {
97        Text('全部待办')
98          .fontSize(30)
99          .fontWeight(FontWeight.Bold)
100      }
101      .width('100%')
102      .margin({top: 10, bottom: 10})
103
104      // 待办事项
105      Row({space: 15}) {
106        if (this.isFinished) {
107          // 此处'app.media.finished'仅作示例,请开发者自行替换,否则imageSource创建失败会导致后续无法正常执行。
108          Image($r('app.media.finished'))
109            .width(28)
110            .height(28)
111        }
112        else {
113          // 此处'app.media.unfinished'仅作示例,请开发者自行替换,否则imageSource创建失败会导致后续无法正常执行。
114          Image($r('app.media.unfinished'))
115            .width(28)
116            .height(28)
117        }
118        Text('学习高数')
119          .fontSize(24)
120          .fontWeight(450)
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![state](./figures/MVVM_state.gif)
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        .fontWeight(450)
206        .decoration({type: this.isFinished ? TextDecorationType.LineThrough : TextDecorationType.None})
207    }
208    .height('40%')
209    .width('100%')
210    .border({width: 5})
211    .padding({left: 15})
212    .onClick(() => {
213      this.isFinished = !this.isFinished;
214    })
215  }
216}
217
218@Component
219struct ThingsComponent2 {
220  @Prop isFinished: boolean;
221
222  build() {
223    // 待办事项1
224    Row({space: 15}) {
225      if (this.isFinished) {
226        // 此处'app.media.finished'仅作示例,请开发者自行替换,否则imageSource创建失败会导致后续无法正常执行。
227        Image($r('app.media.finished'))
228          .width(28)
229          .height(28)
230      }
231      else {
232        // 此处'app.media.unfinished'仅作示例,请开发者自行替换,否则imageSource创建失败会导致后续无法正常执行。
233        Image($r('app.media.unfinished'))
234          .width(28)
235          .height(28)
236      }
237      Text('学习高数')
238        .fontSize(24)
239        .fontWeight(450)
240        .decoration({type: this.isFinished ? TextDecorationType.LineThrough : TextDecorationType.None})
241    }
242    .height('40%')
243    .width('100%')
244    .border({width: 5})
245    .padding({left: 15})
246    .onClick(() => {
247      this.isFinished = !this.isFinished;
248    })
249  }
250}
251
252@Entry
253@Component
254struct Index {
255  @State isFinished: boolean = false;
256
257  build() {
258    Column() {
259      // 全部待办
260      TodoComponent()
261
262      // 全选
263      AllChooseComponent({isFinished: this.isFinished})
264
265      // 待办事项1
266      ThingsComponent1({isFinished: this.isFinished})
267
268      // 待办事项2
269      ThingsComponent2({isFinished: this.isFinished})
270    }
271    .height('100%')
272    .width('100%')
273    .margin({top: 5, bottom: 5})
274    .backgroundColor('#90f1f3f5')
275  }
276}
277```
278
279效果图如下:
280
281![Prop&Link](./figures/MVVM_Prop&Link.gif)
282
283### 循环渲染组件
284
285* 上个示例虽然拆分出了子组件,但是发现组件1和组件2的代码十分类似,当渲染的组件除了数据外其他设置都相同时,此时就需要使用到ForEach循环渲染。
286* ForEach使用之后,冗余代码变得更少,并且代码结构更加清晰。
287
288```typescript
289@Component
290struct TodoComponent {
291  build() {
292    Row() {
293      Text('全部待办')
294        .fontSize(30)
295        .fontWeight(FontWeight.Bold)
296    }
297    .width('100%')
298    .margin({top: 10, bottom: 10})
299  }
300}
301
302@Component
303struct AllChooseComponent {
304  @Link isFinished: boolean;
305
306  build() {
307    Row() {
308      Button('全选', {type: ButtonType.Normal})
309        .onClick(() => {
310          this.isFinished = !this.isFinished;
311        })
312        .fontSize(30)
313        .fontWeight(FontWeight.Bold)
314        .backgroundColor('#f7f6cc74')
315    }
316    .padding({left: 15})
317    .width('100%')
318    .margin({top: 10, bottom: 10})
319  }
320}
321
322@Component
323struct ThingsComponent {
324  @Prop isFinished: boolean;
325  @Prop things: string;
326  build() {
327    // 待办事项1
328    Row({space: 15}) {
329      if (this.isFinished) {
330        // 此处'app.media.finished'仅作示例,请开发者自行替换,否则imageSource创建失败会导致后续无法正常执行。
331        Image($r('app.media.finished'))
332          .width(28)
333          .height(28)
334      }
335      else {
336        // 此处'app.media.unfinished'仅作示例,请开发者自行替换,否则imageSource创建失败会导致后续无法正常执行。
337        Image($r('app.media.unfinished'))
338          .width(28)
339          .height(28)
340      }
341      Text(`${this.things}`)
342        .fontSize(24)
343        .fontWeight(450)
344        .decoration({type: this.isFinished ? TextDecorationType.LineThrough : TextDecorationType.None})
345    }
346    .height('8%')
347    .width('90%')
348    .padding({left: 15})
349    .opacity(this.isFinished ? 0.3: 1)
350    .border({width:1})
351    .borderColor(Color.White)
352    .borderRadius(25)
353    .backgroundColor(Color.White)
354    .onClick(() => {
355      this.isFinished = !this.isFinished;
356    })
357  }
358}
359
360@Entry
361@Component
362struct Index {
363  @State isFinished: boolean = false;
364  @State planList: string[] = [
365    '7.30 起床',
366    '8.30 早餐',
367    '11.30 中餐',
368    '17.30 晚餐',
369    '21.30 夜宵',
370    '22.30 洗澡',
371    '1.30 起床'
372  ];
373
374  build() {
375    Column() {
376      // 全部待办
377      TodoComponent()
378
379      // 全选
380      AllChooseComponent({isFinished: this.isFinished})
381
382      List() {
383        ForEach(this.planList, (item: string) => {
384          // 待办事项1
385          ThingsComponent({isFinished: this.isFinished, things: item})
386            .margin(5)
387        })
388      }
389
390    }
391    .height('100%')
392    .width('100%')
393    .margin({top: 5, bottom: 5})
394    .backgroundColor('#90f1f3f5')
395  }
396}
397```
398
399效果图如下:
400
401![ForEach](./figures/MVVM_ForEach.gif)
402
403### @Builder方法
404
405* Builder方法用于组件内定义方法,可以使得相同代码可以在组件内进行复用。
406* 本示例不仅使用了@Builder方法进行去重,同时对数据进行了移出,可以看到此时代码更加清晰易读,相对于最开始的代码,@Entry组件基本只用于处理页面构建逻辑,而不处理大量与页面设计无关的内容。
407
408```typescript
409@Observed
410class TodoListData {
411  planList: string[] = [
412    '7.30 起床',
413    '8.30 早餐',
414    '11.30 中餐',
415    '17.30 晚餐',
416    '21.30 夜宵',
417    '22.30 洗澡',
418    '1.30 起床'
419  ];
420}
421
422@Component
423struct TodoComponent {
424  build() {
425    Row() {
426      Text('全部待办')
427        .fontSize(30)
428        .fontWeight(FontWeight.Bold)
429    }
430    .width('100%')
431    .margin({top: 10, bottom: 10})
432  }
433}
434
435@Component
436struct AllChooseComponent {
437  @Link isFinished: boolean;
438
439  build() {
440    Row() {
441      Button('全选', {type: ButtonType.Capsule})
442        .onClick(() => {
443          this.isFinished = !this.isFinished;
444        })
445        .fontSize(30)
446        .fontWeight(FontWeight.Bold)
447        .backgroundColor('#f7f6cc74')
448    }
449    .padding({left: 15})
450    .width('100%')
451    .margin({top: 10, bottom: 10})
452  }
453}
454
455@Component
456struct ThingsComponent {
457  @Prop isFinished: boolean;
458  @Prop things: string;
459
460  @Builder displayIcon(icon: Resource) {
461    Image(icon)
462      .width(28)
463      .height(28)
464      .onClick(() => {
465        this.isFinished = !this.isFinished;
466      })
467  }
468
469  build() {
470    // 待办事项1
471    Row({space: 15}) {
472      if (this.isFinished) {
473        // 此处'app.media.finished'仅作示例,请开发者自行替换,否则imageSource创建失败会导致后续无法正常执行。
474        this.displayIcon($r('app.media.finished'));
475      }
476      else {
477        // 此处'app.media.unfinished'仅作示例,请开发者自行替换,否则imageSource创建失败会导致后续无法正常执行。
478        this.displayIcon($r('app.media.unfinished'));
479      }
480      Text(`${this.things}`)
481        .fontSize(24)
482        .fontWeight(450)
483        .decoration({type: this.isFinished ? TextDecorationType.LineThrough : TextDecorationType.None})
484        .onClick(() => {
485          this.things += '啦';
486        })
487    }
488    .height('8%')
489    .width('90%')
490    .padding({left: 15})
491    .opacity(this.isFinished ? 0.3: 1)
492    .border({width:1})
493    .borderColor(Color.White)
494    .borderRadius(25)
495    .backgroundColor(Color.White)
496  }
497}
498
499@Entry
500@Component
501struct Index {
502  @State isFinished: boolean = false;
503  @State data: TodoListData = new TodoListData();
504
505  build() {
506    Column() {
507      // 全部待办
508      TodoComponent()
509
510      // 全选
511      AllChooseComponent({isFinished: this.isFinished})
512
513      List() {
514        ForEach(this.data.planList, (item: string) => {
515          // 待办事项1
516          ThingsComponent({isFinished: this.isFinished, things: item})
517            .margin(5)
518        })
519      }
520
521    }
522    .height('100%')
523    .width('100%')
524    .margin({top: 5, bottom: 5})
525    .backgroundColor('#90f1f3f5')
526  }
527}
528```
529
530 效果图如下:
531
532![builder](./figures/MVVM_builder.gif)
533
534### 总结
535
536* 通过对代码结构的一步步优化,可以看到@Enrty组件作为页面的入口,其build函数应该只需要考虑将需要的组件进行组合,类似于搭积木,将需要的组件搭起来。被page调用的子组件则类似积木,等着被需要的page进行调用。状态变量类似于粘合剂,当触发UI刷新事件时,状态变量能自动完成对应绑定的组件的刷新,从而实现page的按需刷新。
537* 虽然现有的架构并未使用到MVVM的设计理念,但是MVVM的核心理念已经呼之欲出,这也是为什么说ArkUI的UI开发天生属于MVVM模式,page和组件就是View层,page负责搭积木,组件就是积木被page组织;组件需要刷新,通过状态变量驱动组件刷新从而更新page;ViewModel的数据需要有来源,这就是Model层来源。
538* 示例中的代码功能还是比较简单的,但是已经感觉到功能越来越多的情况下,主page的代码越来越多,当备忘录需要添加的功能越来越多时,其他的page也需要使用到主page的组件时,应该如何去组织项目结构呢,MVVM模式是组织的首选。
539
540## 通过MVVM开发备忘录实战
541
542上一章节中,展示了非MVVM模式如何组织代码,能感觉到随着主page的代码越来越庞大,应该采取合理的方式进行分层,使得项目结构清晰,组件之间不去互相引用,导致后期维护时牵一发而动全身,加大后期功能更新的困难,为此本章通过对MVVM的核心文件组织模式介绍入手,向开发者展示如何使用MVVM来组织上一章节的代码。
543
544### MVVM文件结构说明
545
546* src
547  * ets
548    * pages ------ 存放页面组件
549    * views ------ 存放业务组件
550    * shares ------ 存放通用组件
551    * service ------ 数据服务
552      * app.ts ------ 服务入口
553      * LoginViewMode ----- 登录页ViewModel
554      * xxxModel ------ 其他页ViewModel
555
556### 分层设计技巧
557
558**Model层**
559
560* model层存放本应用核心数据结构,这层本身和UI开发关系不大,让用户按照自己的业务逻辑进行封装。
561
562**ViewModel层**
563
564> 注意:
565>
566> ViewModel层不只是存放数据,他同时需要提供数据的服务及处理,因此很多框架会以“service”来进行表达此层。
567
568* ViewModel层是为视图服务的数据层。它的设计一般来说,有两个特点:
569  1、按照页面组织数据。
570  2、每个页面数据进行懒加载。
571
572**View层**
573
574View层根据需要来组织,但View层需要区分一下三种组件:
575
576* 页面组件:提供整体页面布局,实现多页面之间的跳转,前后台事件处理等页面内容。
577* 业务组件:被页面引用,构建出页面。
578* 共享组件:与项目无关的多项目共享组件。
579
580> 共享组件和业务组件的区别:
581>
582> 业务组件包含了ViewModel层数据,没有ViewModel,这个组件不能运行。
583>
584> 共享组件:不包含任务ViewModel层的数据,他需要的数据需要从外部传入。共享组件包含一个自包含组件,只要外部参数(无业务参数)满足,就可以工作。
585
586### 代码示例
587
588现在按照MVVM模式组织结构,重构如下:
589
590* src
591  * ets
592    * pages
593      * index
594    * View
595      * TodoComponent
596      * AllchooseComponent
597      * ThingsComponent
598    * ViewModel
599      * ThingsViewModel
600
601文件代码如下:
602
603* Index.ets
604
605  ```typescript
606  // import view
607  import { TodoComponent } from './../View/TodoComponent'
608  import { MultiChooseComponent } from './../View/AllchooseComponent'
609  import { ThingsComponent } from './../View/ThingsComponent'
610
611  // import viewModel
612  import { TodoListData } from '../ViewModel/ThingsViewModel'
613
614  @Entry
615  @Component
616  struct Index {
617    @State isFinished: boolean = false;
618    @State data: TodoListData = new TodoListData();
619
620    build() {
621      Column() {
622        Row({space: 40}) {
623          // 全部待办
624          TodoComponent()
625
626          // 全选
627          MultiChooseComponent({isFinished: this.isFinished})
628        }
629
630        List() {
631          ForEach(this.data.planList, (item: string) => {
632            // 待办事项1
633            ThingsComponent({isFinished: this.isFinished, things: item})
634              .margin(5)
635          })
636        }
637
638      }
639      .height('100%')
640      .width('100%')
641      .margin({top: 5, bottom: 5})
642      .backgroundColor('#90f1f3f5')
643    }
644  }
645  ```
646
647  * TodoComponent
648
649  ```typescript
650  @Component
651  export struct TodoComponent {
652    build() {
653      Row() {
654        Text('全部待办')
655          .fontSize(30)
656          .fontWeight(FontWeight.Bold)
657      }
658      .padding({left: 15})
659      .width('50%')
660      .margin({top: 10, bottom: 10})
661    }
662  }
663  ```
664
665
666
667  * AllchooseComponent.ets
668
669  ```typescript
670  @Component
671  export struct MultiChooseComponent {
672    @Link isFinished: boolean;
673
674    build() {
675      Row() {
676        Button('多选', {type: ButtonType.Capsule})
677          .onClick(() => {
678            this.isFinished = !this.isFinished;
679          })
680          .fontSize(30)
681          .fontWeight(FontWeight.Bold)
682          .backgroundColor('#f7f6cc74')
683      }
684      .padding({left: 15})
685      .width('100%')
686      .margin({top: 10, bottom: 10})
687    }
688  }
689  ```
690
691  * ThingsComponent
692
693  ```typescript
694  @Component
695  export struct ThingsComponent {
696    @Prop isFinished: boolean;
697    @Prop things: string;
698
699    @Builder displayIcon(icon: Resource) {
700      Image(icon)
701        .width(28)
702        .height(28)
703        .onClick(() => {
704          this.isFinished = !this.isFinished;
705        })
706    }
707
708    build() {
709      // 待办事项1
710      Row({space: 15}) {
711        if (this.isFinished) {
712          // 此处'app.media.finished'仅作示例,请开发者自行替换,否则imageSource创建失败会导致后续无法正常执行。
713          this.displayIcon($r('app.media.finished'));
714        }
715        else {
716          // 此处'app.media.unfinished'仅作示例,请开发者自行替换,否则imageSource创建失败会导致后续无法正常执行。
717          this.displayIcon($r('app.media.unfinished'));
718        }
719        Text(`${this.things}`)
720          .fontSize(24)
721          .fontWeight(450)
722          .decoration({type: this.isFinished ? TextDecorationType.LineThrough : TextDecorationType.None})
723          .onClick(() => {
724            this.things += '啦';
725          })
726      }
727      .height('8%')
728      .width('90%')
729      .padding({left: 15})
730      .opacity(this.isFinished ? 0.3: 1)
731      .border({width:1})
732      .borderColor(Color.White)
733      .borderRadius(25)
734      .backgroundColor(Color.White)
735    }
736  }
737
738  ```
739
740  ThingsViewModel.ets
741
742  ```typescript
743  @Observed
744  export class TodoListData {
745    planList: string[] = [
746      '7.30 起床',
747      '8.30 早餐',
748      '11.30 中餐',
749      '17.30 晚餐',
750      '21.30 夜宵',
751      '22.30 洗澡',
752      '1.30 起床'
753    ];
754  }
755  ```
756
757  经过MVVM模式拆分之后的代码,项目结构更加清晰,各个模块的职责更加清晰,假如有新的page需要用到事件这个组件,只需要import对应的组件即可,因为是固定的本地数据,没有去写Model层的逻辑,后续开发者也可以照着示例去重构自己的项目结构。
758
759  效果图如下:
760
761  ![MVVM_index.gif](./figures/MVVM_index.gif)
762
763
764
765
766
767