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