1# MVVM模式(状态管理V2)
2
3## 概述
4
5在应用开发中,UI的更新需要随着数据状态的变化进行实时同步,而这种同步往往决定了应用程序的性能和用户体验。为了解决数据与UI同步的复杂性,ArkUI采用了Model-View-ViewModel(MVVM)架构模式。MVVM将应用分为Model、View和ViewModel三个核心部分,实现数据、视图与逻辑的分离。通过这种模式,UI可以随着状态的变化自动更新,无需手动处理,从而更加高效地管理数据和视图的绑定与更新。
6
7- Model:负责存储和管理应用的数据以及业务逻辑,不直接与用户界面交互。通常从后端接口获取数据,是应用程序的数据基础,确保数据的一致性和完整性。
8- View:负责用户界面展示数据并与用户交互,不包含任何业务逻辑。它通过绑定ViewModel层提供的数据来动态更新UI。
9- ViewModel:负责管理UI状态和交互逻辑。作为连接Model和View的桥梁,ViewModel监控Model数据的变化,通知View更新UI,同时处理用户交互事件并转换为数据操作。
10
11
12## 通过状态管理V2版本实现ViewModel
13
14在MVVM模式中,ViewModel扮演着至关重要的角色,负责管理数据状态,并在数据发生变化时自动更新视图。ArkUI的状态管理V2版本提供了丰富的装饰器和工具,帮助开发者在自定义组件之间共享数据,确保数据变化自动同步到UI。常用的状态管理装饰器包括\@Local、\@Param、\@Event、\@ObservedV2、\@Trace等等。除此之外,V2还提供了AppStorageV2和PersistenceV2作为全局状态存储工具,用于应用间的状态共享和持久化存储。
15
16本节将通过一个简单的todolist示例,逐步引入和使用状态管理V2的装饰器及工具,从基础的静态任务列表开始,逐步扩展功能。每个步骤都基于上一步扩展,帮助开发者循序渐进地理解并掌握各个装饰器的使用方法。
17
18### 基础示例
19
20首先,从最基础的静态待办事项列表开始。在这个例子中,任务是静态的,没有状态变化和动态交互。
21
22```ts
23@Entry
24@ComponentV2
25struct TodoList {
26  build() {
27    Column() {
28      Text('待办')
29        .fontSize(40)
30        .margin({ bottom: 10 })
31      Text('Task1')
32      Text('Task2')
33      Text('Task3')
34    }
35  }
36}
37```
38
39### 添加\@Local,实现对组件内部状态观测
40
41完成静态待办列表展示后,为了让用户能够更改任务的完成状态,需要使待办事项能够响应交互并动态更新显示。为此,引入\@Local装饰器管理组件内部的状态。被\@Local装饰的变量发生变化时,会触发绑定的UI组件刷新。
42
43在这个例子中,新增了一个被\@Local装饰的isFinish属性代表任务是否完成。准备了两个图标:finished.pngunfinished.png,用于展示任务完成或未完成的状态。点击待办事项时,isFinish状态切换,从而更新图标和文本删除线的效果。
44
45```ts
46@Entry
47@ComponentV2
48struct TodoList {
49  @Local isFinish: boolean = false;
50
51  build() {
52    Column() {
53      Text('待办')
54        .fontSize(40)
55        .margin({ bottom: 10 })
56      Row() {
57        // 请开发者自行在src/main/resources/base/media路径下添加finished.pngunfinished.png两张图片,否则运行时会因资源缺失而报错
58        Image(this.isFinish ? $r('app.media.finished') : $r('app.media.unfinished'))
59          .width(28)
60          .height(28)
61        Text('Task1')
62          .decoration({ type: this.isFinish ? TextDecorationType.LineThrough : TextDecorationType.None })
63      }
64      .onClick(() => this.isFinish = !this.isFinish)
65    }
66  }
67}
68```
69
70### 添加\@Param,实现组件接受外部输入
71实现了任务本地状态切换后,为了增强待办事项列表的灵活性,需要能够动态设置每个任务的名称,而不是固定在代码中。引入\@Param装饰器后,子组件被修饰的变量能够接收父组件传入的值,实现从父到子的单向数据同步。默认情况下,\@Param是只读的。如需在子组件中对传入的值进行本地更新,可使用\@Param \@Once进行配置。
72
73在这个例子中,每个待办事项被抽象为TaskItem组件。被\@Param修饰的taskName属性从父组件TodoList传入任务名称,使TaskItem组件灵活且可复用,能接收并渲染不同的任务名称。被\@Param \@Once装饰的isFinish属性在接收初始值后,可以在子组件内更新。
74
75```ts
76@ComponentV2
77struct TaskItem {
78  @Param taskName: string = '';
79  @Param @Once isFinish: boolean = false;
80
81  build() {
82    Row() {
83      // 请开发者自行在src/main/resources/base/media路径下添加finished.pngunfinished.png两张图片,否则运行时会因资源缺失而报错
84      Image(this.isFinish ? $r('app.media.finished') : $r('app.media.unfinished'))
85        .width(28)
86        .height(28)
87      Text(this.taskName)
88        .decoration({ type: this.isFinish ? TextDecorationType.LineThrough : TextDecorationType.None })
89    }
90    .onClick(() => this.isFinish = !this.isFinish)
91  }
92}
93
94@Entry
95@ComponentV2
96struct TodoList {
97  build() {
98    Column() {
99      Text('待办')
100        .fontSize(40)
101        .margin({ bottom: 10 })
102      TaskItem({ taskName: 'Task 1', isFinish: false })
103      TaskItem({ taskName: 'Task 2', isFinish: false })
104      TaskItem({ taskName: 'Task 3', isFinish: false })
105    }
106  }
107}
108```
109
110### 添加\@Event,实现组件对外输出
111
112在实现任务名称动态设置后,任务列表内容依然是固定的,需要增加任务项的添加和删除功能,以实现任务列表的动态扩展。为此,引入\@Event装饰器,用于实现子组件向父组件输出数据。
113
114在这个例子中,每个TaskItem增加了删除按钮,同时任务列表底部增加了添加新任务的功能。点击子组件TaskItem的“删除”按钮时,deleteTask事件会被触发并传递给父组件TodoList,父组件响应并将该任务从列表中移除。通过使用\@Param和\@Event,子组件不仅能接收父组件的数据,还能够将事件传递回父组件,实现数据的双向同步。
115
116```ts
117@ComponentV2
118struct TaskItem {
119  @Param taskName: string = '';
120  @Param @Once isFinish: boolean = false;
121  @Event deleteTask: () => void = () => {};
122
123  build() {
124    Row() {
125      // 请开发者自行在src/main/resources/base/media路径下添加finished.pngunfinished.png两张图片,否则运行时会因资源缺失而报错
126      Image(this.isFinish ? $r('app.media.finished') : $r('app.media.unfinished'))
127        .width(28)
128        .height(28)
129      Text(this.taskName)
130        .decoration({ type: this.isFinish ? TextDecorationType.LineThrough : TextDecorationType.None })
131      Button('删除')
132        .onClick(() => this.deleteTask())
133    }
134    .onClick(() => this.isFinish = !this.isFinish)
135  }
136}
137
138@Entry
139@ComponentV2
140struct TodoList {
141  @Local tasks: string[] = ['task1','task2','task3'];
142  @Local newTaskName: string = '';
143  build() {
144    Column() {
145      Text('待办')
146        .fontSize(40)
147        .margin({ bottom: 10 })
148      ForEach(this.tasks, (task: string) => {
149          TaskItem({
150            taskName: task,
151            isFinish: false,
152            deleteTask: () => this.tasks.splice(this.tasks.indexOf(task), 1)
153          })
154      })
155      Row() {
156        TextInput({ placeholder: '添加新任务', text: this.newTaskName })
157          .onChange((value) => this.newTaskName = value)
158          .width('70%')
159        Button('增加事项')
160          .onClick(() => {
161            this.tasks.push(this.newTaskName);
162            this.newTaskName = '';
163          })
164      }
165    }
166  }
167}
168```
169
170### 添加Repeat,实现子组件复用
171
172添加了任务增删功能后,随着任务列表项的增加,需要一种高效渲染多个结构相同的子组件的方法,以提高界面的性能表现。为此,引入了Repeat方法,用于优化任务列表的渲染过程。Repeat支持两种模式:virtualScroll和non-virtualScroll。virtualScroll适用于大量数据的场景,在滚动类容器中按需加载组件,极大节省内存和提升渲染效率。non-virtualScroll适用于数据量较小的场景,一次性渲染所有组件,并在数据变化时仅更新需要变化的部分,避免整体重新渲染。
173
174在本例中,任务量较少,选择了non-virtualScroll模式。新建了一个任务数组tasks,并使用Repeat方法迭代数组中的每一项,动态生成并复用TaskItem组件。在任务增删时,这种方式能高效复用已有组件,避免重复渲染,从而提高界面响应速度和性能。这种机制有效地提高了代码的复用性和渲染效率。
175
176```ts
177@ComponentV2
178struct TaskItem {
179  @Param taskName: string = '';
180  @Param @Once isFinish: boolean = false;
181  @Event deleteTask: () => void = () => {};
182
183  build() {
184    Row() {
185      // 请开发者自行在src/main/resources/base/media路径下添加finished.pngunfinished.png两张图片,否则运行时会因资源缺失而报错
186      Image(this.isFinish ? $r('app.media.finished') : $r('app.media.unfinished'))
187        .width(28)
188        .height(28)
189      Text(this.taskName)
190        .decoration({ type: this.isFinish ? TextDecorationType.LineThrough : TextDecorationType.None })
191      Button('删除')
192        .onClick(() => this.deleteTask())
193    }
194    .onClick(() => this.isFinish = !this.isFinish)
195  }
196}
197
198@Entry
199@ComponentV2
200struct TodoList {
201  @Local tasks: string[] = ['task1','task2','task3'];
202  @Local newTaskName: string = '';
203  build() {
204    Column() {
205      Text('待办')
206        .fontSize(40)
207        .margin({ bottom: 10 })
208      Repeat<string>(this.tasks)
209        .each((obj: RepeatItem<string>) => {
210          TaskItem({
211            taskName: obj.item,
212            isFinish: false,
213            deleteTask: () => this.tasks.splice(this.tasks.indexOf(obj.item), 1)
214          })
215        })
216      Row() {
217        TextInput({ placeholder: '添加新任务', text: this.newTaskName })
218          .onChange((value) => this.newTaskName = value)
219          .width('70%')
220        Button('增加事项')
221          .onClick(() => {
222            this.tasks.push(this.newTaskName);
223            this.newTaskName = '';
224          })
225      }
226    }
227  }
228}
229```
230
231### 添加\@ObservedV2,\@Trace,实现类属性观测变化
232
233实现了多个功能之后,任务列表的管理逐渐变得复杂。为了更好地处理任务数据的变化,特别在多层嵌套结构中,需要确保属性的变化可以被深度观测并自动更新UI。为此,引入了\@ObservedV2和\@Trace装饰器。相比于\@Local只能观测对象本身及其第一层的变化,\@ObservedV2和\@Trace更适用于处理多层嵌套、继承等复杂结构场景。在\@ObservedV2装饰的类中,被\@Trace装饰的属性发生变化时,会触发其绑定的UI组件刷新。
234
235在这个例子中,任务(Task)被抽象为一个类,并用\@ObservedV2标记该类,用\@Trace标记isFinish属性。TodoList组件嵌套了TaskItem,TaskItem又嵌套了Task。在最外层的TodoList中,添加了"全部完成"和"全部未完成"的按钮,每次点击这些按钮都会直接更新最内层Task类的isFinish属性。\@ObservedV2和\@Trace确保可以观察到对应isFinish UI组件的刷新,从而实现了对嵌套类属性的深度观测。
236
237```ts
238@ObservedV2
239class Task {
240  taskName: string = '';
241  @Trace isFinish: boolean = false;
242
243  constructor (taskName: string, isFinish: boolean) {
244    this.taskName = taskName;
245    this.isFinish = isFinish;
246  }
247}
248
249@ComponentV2
250struct TaskItem {
251  @Param task: Task = new Task('', false);
252  @Event deleteTask: () => void = () => {};
253
254  build() {
255    Row() {
256      // 请开发者自行在src/main/resources/base/media路径下添加finished.pngunfinished.png两张图片,否则运行时会因资源缺失而报错
257      Image(this.task.isFinish ? $r('app.media.finished') : $r('app.media.unfinished'))
258        .width(28)
259        .height(28)
260      Text(this.task.taskName)
261        .decoration({ type: this.task.isFinish ? TextDecorationType.LineThrough : TextDecorationType.None })
262      Button('删除')
263        .onClick(() => this.deleteTask())
264    }
265    .onClick(() => this.task.isFinish = !this.task.isFinish)
266  }
267}
268
269@Entry
270@ComponentV2
271struct TodoList {
272  @Local tasks: Task[] = [
273    new Task('task1', false),
274    new Task('task2', false),
275    new Task('task3', false),
276  ];
277  @Local newTaskName: string = '';
278
279  finishAll(ifFinish: boolean) {
280    for (let task of this.tasks) {
281      task.isFinish = ifFinish;
282    }
283  }
284
285  build() {
286    Column() {
287      Text('待办')
288        .fontSize(40)
289        .margin({ bottom: 10 })
290      Repeat<Task>(this.tasks)
291        .each((obj: RepeatItem<Task>) => {
292          TaskItem({
293            task: obj.item,
294            deleteTask: () => this.tasks.splice(this.tasks.indexOf(obj.item), 1)
295          })
296        })
297      Row() {
298        Button('全部完成')
299          .onClick(() => this.finishAll(true))
300        Button('全部未完成')
301          .onClick(() => this.finishAll(false))
302      }
303      Row() {
304        TextInput({ placeholder: '添加新任务', text: this.newTaskName })
305          .onChange((value) => this.newTaskName = value)
306          .width('70%')
307        Button('增加事项')
308          .onClick(() => {
309            this.tasks.push(new Task(this.newTaskName, false));
310            this.newTaskName = '';
311          })
312      }
313    }
314  }
315}
316```
317
318### 添加\@Monitor,\@Computed,实现监听状态变量和计算属性
319
320在当前任务列表功能基础上,为了提升体验,可以增加一些额外的功能,如任务状态变化的监听和未完成任务数量的动态计算。为此,引入\@Monitor和\@Computed装饰器。\@Monitor用于深度监听状态变量,在属性变化时触发自定义回调方法。\@Computed用于装饰getter方法,检测被计算的属性变化。在被计算的值变化时,仅会计算一次,减少重复计算开销。
321
322在这个例子中,使用\@Monitor深度监听TaskItem中task的isFinish属性。当任务完成状态变化时会触发onTasksFinished回调,输出日志记录任务完成状态的变化。此外,新增了对todolist中未完成任务的数量的记录。用\@Computed装饰tasksUnfinished,每当任务状态变化时自动重新计算。通过这两个装饰器,实现了对状态变量的深度监听和高效的计算属性。
323
324```ts
325@ObservedV2
326class Task {
327  taskName: string = '';
328  @Trace isFinish: boolean = false;
329
330  constructor (taskName: string, isFinish: boolean) {
331    this.taskName = taskName;
332    this.isFinish = isFinish;
333  }
334}
335
336@ComponentV2
337struct TaskItem {
338  @Param task: Task = new Task('', false);
339  @Event deleteTask: () => void = () => {};
340  @Monitor('task.isFinish')
341  onTaskFinished(mon: IMonitor) {
342    console.log('任务' + this.task.taskName + '的完成状态从' + mon.value()?.before + '变为了' + mon.value()?.now);
343  }
344
345  build() {
346    Row() {
347      // 请开发者自行在src/main/resources/base/media路径下添加finished.pngunfinished.png两张图片,否则运行时会因资源缺失而报错
348      Image(this.task.isFinish ? $r('app.media.finished') : $r('app.media.unfinished'))
349        .width(28)
350        .height(28)
351      Text(this.task.taskName)
352        .decoration({ type: this.task.isFinish ? TextDecorationType.LineThrough : TextDecorationType.None })
353      Button('删除')
354        .onClick(() => this.deleteTask())
355    }
356    .onClick(() => this.task.isFinish = !this.task.isFinish)
357  }
358}
359
360@Entry
361@ComponentV2
362struct TodoList {
363  @Local tasks: Task[] = [
364    new Task('task1', false),
365    new Task('task2', false),
366    new Task('task3', false),
367  ];
368  @Local newTaskName: string = '';
369
370  finishAll(ifFinish: boolean) {
371    for (let task of this.tasks) {
372      task.isFinish = ifFinish;
373    }
374  }
375
376  @Computed
377  get tasksUnfinished(): number {
378    return this.tasks.filter(task => !task.isFinish).length;
379  }
380
381  build() {
382    Column() {
383      Text('待办')
384        .fontSize(40)
385        .margin({ bottom: 10 })
386      Text(`未完成任务:${this.tasksUnfinished}`)
387      Repeat<Task>(this.tasks)
388        .each((obj: RepeatItem<Task>) => {
389          TaskItem({
390            task: obj.item,
391            deleteTask: () => this.tasks.splice(this.tasks.indexOf(obj.item), 1)
392          })
393        })
394      Row() {
395        Button('全部完成')
396          .onClick(() => this.finishAll(true))
397        Button('全部未完成')
398          .onClick(() => this.finishAll(false))
399      }
400      Row() {
401        TextInput({ placeholder: '添加新任务', text: this.newTaskName })
402          .onChange((value) => this.newTaskName = value)
403          .width('70%')
404        Button('增加事项')
405          .onClick(() => {
406            this.tasks.push(new Task(this.newTaskName, false));
407            this.newTaskName = '';
408          })
409      }
410    }
411  }
412}
413```
414
415### 添加AppStorageV2,实现应用全局UI状态存储
416
417随着待办事项功能的不断增强,应用可能涉及到多个页面或功能模块,此时常常需要在这些页面之间共享全局状态。例如,在待办事项应用中,可以新增一个设置页面与主界面联动。为实现跨页面的状态共享,引入AppStorageV2,用于在多个UIAbility实例之间存储和共享应用的全局状态。
418
419在这个例子中,新增了一个Ability,SettingAbility,用于加载设置页SettingPage。SettingPage包含了一个Setting类,其中的showCompletedTask属性用于控制是否显示已完成的任务,用户通过一个开关可以切换该选项。两个Ability通过AppStorageV2共享设置数据,键为"Setting",对应的数据为Setting类。第一次通过connect连接Setting时,若不存在储存的数据,会新建一个默认showCompletedTask为trueSetting实例。后续用户在设置页面修改设置后,主页面会根据这一设置更新任务列表的显示。通过AppStorageV2,实现了跨Ability、跨页面的数据共享。
420
421```ts
422import { AppStorageV2 } from '@kit.ArkUI';
423import { common, Want } from '@kit.AbilityKit';
424import { Setting } from './SettingPage';
425
426@ObservedV2
427class Task {
428  taskName: string = '';
429  @Trace isFinish: boolean = false;
430
431  constructor (taskName: string, isFinish: boolean) {
432    this.taskName = taskName;
433    this.isFinish = isFinish;
434  }
435}
436
437@ComponentV2
438struct TaskItem {
439  @Param task: Task = new Task('', false);
440  @Event deleteTask: () => void = () => {};
441  @Monitor('task.isFinish')
442  onTaskFinished(mon: IMonitor) {
443    console.log('任务' + this.task.taskName + '的完成状态从' + mon.value()?.before + '变为了' + mon.value()?.now);
444  }
445
446  build() {
447    Row() {
448      // 请开发者自行在src/main/resources/base/media路径下添加finished.pngunfinished.png两张图片,否则运行时会因资源缺失而报错
449      Image(this.task.isFinish ? $r('app.media.finished') : $r('app.media.unfinished'))
450        .width(28)
451        .height(28)
452      Text(this.task.taskName)
453        .decoration({ type: this.task.isFinish ? TextDecorationType.LineThrough : TextDecorationType.None })
454      Button('删除')
455        .onClick(() => this.deleteTask())
456    }
457    .onClick(() => this.task.isFinish = !this.task.isFinish)
458  }
459}
460
461@Entry
462@ComponentV2
463struct TodoList {
464  @Local tasks: Task[] = [
465    new Task('task1', false),
466    new Task('task2', false),
467    new Task('task3', false),
468  ];
469  @Local newTaskName: string = '';
470  @Local setting: Setting = AppStorageV2.connect(Setting, 'Setting', () => new Setting())!;
471  private context = getContext(this) as common.UIAbilityContext;
472
473  finishAll(ifFinish: boolean) {
474    for (let task of this.tasks) {
475      task.isFinish = ifFinish;
476    }
477  }
478
479  @Computed
480  get tasksUnfinished(): number {
481    return this.tasks.filter(task => !task.isFinish).length;
482  }
483
484  build() {
485    Column() {
486      Text('待办')
487        .fontSize(40)
488        .margin({ bottom: 10 })
489      Text(`未完成任务:${this.tasksUnfinished}`)
490      Repeat<Task>(this.tasks.filter(task => this.setting.showCompletedTask || !task.isFinish))
491        .each((obj: RepeatItem<Task>) => {
492          TaskItem({
493            task: obj.item,
494            deleteTask: () => this.tasks.splice(this.tasks.indexOf(obj.item), 1)
495          })
496        })
497      Row() {
498        Button('全部完成')
499          .onClick(() => this.finishAll(true))
500        Button('全部未完成')
501          .onClick(() => this.finishAll(false))
502        Button('设置')
503          .onClick(() => {
504            let wantInfo: Want = {
505              deviceId: '', // deviceId为空表示本设备
506              bundleName: 'com.example.mvvmv2_new', // 替换成AppScope/app.json5里的bundleName
507              abilityName: 'SettingAbility',
508            };
509            this.context.startAbility(wantInfo);
510          })
511      }
512      Row() {
513        TextInput({ placeholder: '添加新任务', text: this.newTaskName })
514          .onChange((value) => this.newTaskName = value)
515          .width('70%')
516        Button('增加事项')
517          .onClick(() => {
518            this.tasks.push(new Task(this.newTaskName, false));
519            this.newTaskName = '';
520          })
521      }
522    }
523  }
524}
525```
526
527```ts
528// SettingAbility的SettingPage页面代码
529import { AppStorageV2 } from '@kit.ArkUI';
530import { common } from '@kit.AbilityKit';
531
532@ObservedV2
533export class Setting {
534  @Trace showCompletedTask: boolean = true;
535}
536
537@Entry
538@ComponentV2
539struct SettingPage {
540  @Local setting: Setting = AppStorageV2.connect(Setting, 'Setting', () => new Setting())!;
541  private context = getContext(this) as common.UIAbilityContext;
542
543  build() {
544    Column() {
545      Text('设置')
546        .fontSize(40)
547        .margin({ bottom: 10 })
548      Row() {
549        Text('显示已完成任务');
550        Toggle({ type: ToggleType.Switch, isOn:this.setting.showCompletedTask })
551          .onChange((isOn) => {
552            this.setting.showCompletedTask = isOn;
553          })
554      }
555      Button('返回待办')
556        .onClick(()=>this.context.terminateSelf())
557        .margin({ top: 10 })
558    }
559    .alignItems(HorizontalAlign.Start)
560  }
561}
562```
563
564### 添加PersistenceV2,实现持久化UI状态存储
565
566为了保证用户在重新打开应用时仍然能够看到之前的任务状态,可以引入持久化存储方案。使用PersistenceV2能够将数据持久化保存在设备磁盘上。与AppStorageV2的运行时内存不同,PersistenceV2能确保即使应用关闭后再启动,数据依然保持不变。
567
568在这个例子中,创建了一个TaskList类,用于通过PersistenceV2持久化存储所有任务信息,键为"TaskList",数据对应TaskList类。第一次通过connect连接TaskList时,如果没有数据,会创建一个默认tasks数组为空的新TaskList实例。在aboutToAppear生命周期函数中,连接到PersistenceV2的TaskList,若无存储任务数据,会从本地文件defaultTasks.json中加载任务并存储到PersistenceV2中。此后,每个任务的完成状态都会同步到PersistenceV2中。这样,即使应用关闭后再次打开,所有任务数据依旧保持不变,实现了持久化的应用状态存储功能。
569
570```ts
571import { AppStorageV2, PersistenceV2, Type } from '@kit.ArkUI';
572import { common, Want } from '@kit.AbilityKit';
573import { Setting } from './SettingPage';
574import util from '@ohos.util';
575
576@ObservedV2
577class Task {
578  // 未实现构造函数,因为@Type当前不支持带参数的构造函数
579  @Trace taskName: string = 'Todo';
580  @Trace isFinish: boolean = false;
581}
582
583@ObservedV2
584class TaskList {
585  // 对于复杂对象需要@Type修饰,确保序列化成功
586  @Type(Task)
587  @Trace tasks: Task[] = [];
588
589  constructor(tasks: Task[]) {
590    this.tasks = tasks;
591  }
592
593  async loadTasks(context: common.UIAbilityContext) {
594    let getJson = await context.resourceManager.getRawFileContent('defaultTasks.json');
595    let textDecoderOptions: util.TextDecoderOptions = { ignoreBOM : true };
596    let textDecoder = util.TextDecoder.create('utf-8',textDecoderOptions);
597    let result = textDecoder.decodeToString(getJson);
598    this.tasks =JSON.parse(result).map((task: Task)=>{
599      let newTask = new Task();
600      newTask.taskName = task.taskName;
601      newTask.isFinish = task.isFinish;
602      return newTask;
603    });
604  }
605}
606
607@ComponentV2
608struct TaskItem {
609  @Param task: Task = new Task();
610  @Event deleteTask: () => void = () => {};
611  @Monitor('task.isFinish')
612  onTaskFinished(mon: IMonitor) {
613    console.log('任务' + this.task.taskName + '的完成状态从' + mon.value()?.before + '变为了' + mon.value()?.now);
614  }
615
616  build() {
617    Row() {
618      // 请开发者自行在src/main/resources/base/media路径下添加finished.pngunfinished.png两张图片,否则运行时会因资源缺失而报错
619      Image(this.task.isFinish ? $r('app.media.finished') : $r('app.media.unfinished'))
620        .width(28)
621        .height(28)
622      Text(this.task.taskName)
623        .decoration({ type: this.task.isFinish ? TextDecorationType.LineThrough : TextDecorationType.None })
624      Button('删除')
625        .onClick(() => this.deleteTask())
626    }
627    .onClick(() => this.task.isFinish = !this.task.isFinish)
628  }
629}
630
631@Entry
632@ComponentV2
633struct TodoList {
634  @Local taskList: TaskList = PersistenceV2.connect(TaskList, 'TaskList', () => new TaskList([]))!;
635  @Local newTaskName: string = '';
636  @Local setting: Setting = AppStorageV2.connect(Setting, 'Setting', () => new Setting())!;
637  private context = getContext(this) as common.UIAbilityContext;
638
639  async aboutToAppear() {
640    this.taskList = PersistenceV2.connect(TaskList, 'TaskList', () => new TaskList([]))!;
641    if (this.taskList.tasks.length == 0) {
642      await this.taskList.loadTasks(this.context);
643    }
644  }
645
646  finishAll(ifFinish: boolean) {
647    for (let task of this.taskList.tasks) {
648      task.isFinish = ifFinish;
649    }
650  }
651
652  @Computed
653  get tasksUnfinished(): number {
654    return this.taskList.tasks.filter(task => !task.isFinish).length;
655  }
656
657  build() {
658    Column() {
659      Text('待办')
660        .fontSize(40)
661        .margin({ bottom: 10 })
662      Text(`未完成任务:${this.tasksUnfinished}`)
663      Repeat<Task>(this.taskList.tasks.filter(task => this.setting.showCompletedTask || !task.isFinish))
664        .each((obj: RepeatItem<Task>) => {
665          TaskItem({
666            task: obj.item,
667            deleteTask: () => this.taskList.tasks.splice(this.taskList.tasks.indexOf(obj.item), 1)
668          })
669        })
670      Row() {
671        Button('全部完成')
672          .onClick(() => this.finishAll(true))
673        Button('全部未完成')
674          .onClick(() => this.finishAll(false))
675        Button('设置')
676          .onClick(() => {
677            let wantInfo: Want = {
678              deviceId: '', // deviceId为空表示本设备
679              bundleName: 'com.example.mvvmv2_new',
680              abilityName: 'SettingAbility',
681            };
682            this.context.startAbility(wantInfo);
683          })
684      }
685      Row() {
686        TextInput({ placeholder: '添加新任务', text: this.newTaskName })
687          .onChange((value) => this.newTaskName = value)
688          .width('70%')
689        Button('增加事项')
690          .onClick(() => {
691            let newTask = new Task();
692            newTask.taskName = this.newTaskName;
693            this.taskList.tasks.push(newTask);
694            this.newTaskName = '';
695          })
696      }
697    }
698  }
699}
700```
701
702JSON文件存放在src/main/resources/rawfile/defaultTasks.json路径下。
703```json
704[
705  {"taskName": "学习ArkTS开发", "isFinish": false},
706  {"taskName": "健身", "isFinish": false},
707  {"taskName": "买水果", "isFinish": true},
708  {"taskName": "取快递", "isFinish": true},
709  {"taskName": "刷题", "isFinish": true}
710]
711```
712
713### 添加\@Builder,实现自定义构建函数
714
715随着应用功能逐步扩展,代码中的某些UI元素开始重复,这不仅增加了代码量,也让维护变得复杂。为了解决这一问题,可以使用\@Builder装饰器,将重复的UI组件抽象成独立的构建方法,便于复用和代码的模块化。
716
717在这个例子中,使用\@Builder定义了ActionButton方法,统一管理各类按钮的文字、样式和点击事件,使代码更简洁、结构更清晰,提升了代码的可维护性。在此基础上,调整了待办事项界面的布局和样式,例如组件的间距、颜色和大小,使UI更美观,最终呈现一个功能完善、界面简洁的待办事项应用。
718
719```ts
720import { AppStorageV2, PersistenceV2, Type } from '@kit.ArkUI';
721import { common, Want } from '@kit.AbilityKit';
722import { Setting } from './SettingPage';
723import util from '@ohos.util';
724
725@ObservedV2
726class Task {
727  // 未实现构造函数,因为@Type当前不支持带参数的构造函数
728  @Trace taskName: string = 'Todo';
729  @Trace isFinish: boolean = false;
730}
731
732@Builder function ActionButton(text: string, onClick:() => void) {
733  Button(text, { buttonStyle: ButtonStyleMode.NORMAL })
734    .onClick(onClick)
735    .margin({ left: 10, right: 10, top: 5, bottom: 5 })
736}
737
738@ObservedV2
739class TaskList {
740  // 对于复杂对象需要@Type修饰,确保序列化成功
741  @Type(Task)
742  @Trace tasks: Task[] = [];
743
744  constructor(tasks: Task[]) {
745    this.tasks = tasks;
746  }
747
748  async loadTasks(context: common.UIAbilityContext) {
749    let getJson = await context.resourceManager.getRawFileContent('defaultTasks.json');
750    let textDecoderOptions: util.TextDecoderOptions = { ignoreBOM : true };
751    let textDecoder = util.TextDecoder.create('utf-8',textDecoderOptions);
752    let result = textDecoder.decodeToString(getJson);
753    this.tasks =JSON.parse(result).map((task: Task)=>{
754      let newTask = new Task();
755      newTask.taskName = task.taskName;
756      newTask.isFinish = task.isFinish;
757      return newTask;
758    });
759  }
760}
761
762@ComponentV2
763struct TaskItem {
764  @Param task: Task = new Task();
765  @Event deleteTask: () => void = () => {};
766  @Monitor('task.isFinish')
767  onTaskFinished(mon: IMonitor) {
768    console.log('任务' + this.task.taskName + '的完成状态从' + mon.value()?.before + '变为了' + mon.value()?.now);
769  }
770
771  build() {
772    Row() {
773      // 请开发者自行在src/main/resources/base/media路径下添加finished.pngunfinished.png两张图片,否则运行时会因资源缺失而报错
774      Image(this.task.isFinish ? $r('app.media.finished') : $r('app.media.unfinished'))
775        .width(28)
776        .height(28)
777        .margin({ left : 15, right : 10 })
778      Text(this.task.taskName)
779        .decoration({ type: this.task.isFinish ? TextDecorationType.LineThrough : TextDecorationType.None })
780        .fontSize(18)
781      ActionButton('删除', () => this.deleteTask())
782    }
783    .height('7%')
784    .width('90%')
785    .backgroundColor('#90f1f3f5')
786    .borderRadius(25)
787    .onClick(() => this.task.isFinish = !this.task.isFinish)
788  }
789}
790
791@Entry
792@ComponentV2
793struct TodoList {
794  @Local taskList: TaskList = PersistenceV2.connect(TaskList, 'TaskList', () => new TaskList([]))!;
795  @Local newTaskName: string = '';
796  @Local setting: Setting = AppStorageV2.connect(Setting, 'Setting', () => new Setting())!;
797  private context = getContext(this) as common.UIAbilityContext;
798
799  async aboutToAppear() {
800    this.taskList = PersistenceV2.connect(TaskList, 'TaskList', () => new TaskList([]))!;
801    if (this.taskList.tasks.length == 0) {
802      await this.taskList.loadTasks(this.context);
803    }
804  }
805
806  finishAll(ifFinish: boolean) {
807    for (let task of this.taskList.tasks) {
808      task.isFinish = ifFinish;
809    }
810  }
811
812  @Computed
813  get tasksUnfinished(): number {
814    return this.taskList.tasks.filter(task => !task.isFinish).length;
815  }
816
817  build() {
818    Column() {
819      Text('待办')
820        .fontSize(40)
821        .margin(10)
822      Text(`未完成任务:${this.tasksUnfinished}`)
823        .margin({ left: 10, bottom: 10 })
824      Repeat<Task>(this.taskList.tasks.filter(task => this.setting.showCompletedTask || !task.isFinish))
825        .each((obj: RepeatItem<Task>) => {
826          TaskItem({
827            task: obj.item,
828            deleteTask: () => this.taskList.tasks.splice(this.taskList.tasks.indexOf(obj.item), 1)
829          }).margin(5)
830        })
831      Row() {
832        ActionButton('全部完成', (): void => this.finishAll(true))
833        ActionButton('全部未完成', (): void => this.finishAll(false))
834        ActionButton('设置', (): void => {
835          let wantInfo: Want = {
836            deviceId: '', // deviceId为空表示本设备
837            bundleName: 'com.example.mvvmv2_new',
838            abilityName: 'SettingAbility',
839          };
840          this.context.startAbility(wantInfo);
841        })
842      }
843      .margin({ top: 10, bottom: 5 })
844      Row() {
845        TextInput({ placeholder: '添加新任务', text: this.newTaskName })
846          .onChange((value) => this.newTaskName = value)
847          .width('70%')
848        ActionButton('+', (): void => {
849          let newTask = new Task();
850          newTask.taskName = this.newTaskName;
851          this.taskList.tasks.push(newTask);
852          this.newTaskName = '';
853        })
854      }
855    }
856    .height('100%')
857    .width('100%')
858    .alignItems(HorizontalAlign.Start)
859    .margin({ left: 15 })
860  }
861}
862```
863
864### 效果图展示
865![todolist](./figures/MVVMV2-todolist.gif)
866
867## 重构代码以符合MVVM架构
868
869前面的例子通过使用一系列的状态管理装饰器,实现了todolist中的数据同步与UI更新。然而,随着应用功能的复杂化,代码的结构变得难以维护,Model、View和ViewModel的职责并没有完全分离,仍然存在一定的耦合。为了更好地组织代码和提升可维护性,使用MVVM模式重构代码,进一步将数据层(Model)、逻辑层(ViewModel)和展示层(View)分离。
870
871### 重构后的代码结构
872```
873/src
874├── /main
875│   ├── /ets
876│   │   ├── /entryability
877│   │   ├── /model
878│   │   │   ├── TaskListModel.ets
879│   │   │   └── TaskModel.ets
880│   │   ├── /pages
881│   │   │   ├── SettingPage.ets
882│   │   │   └── TodoListPage.ets
883│   │   ├── /settingability
884│   │   ├── /view
885│   │   │   ├── BottomView.ets
886│   │   │   ├── ListView.ets
887│   │   │   └── TitleView.ets
888│   │   ├── /viewmodel
889│   │   │   ├── TaskListViewModel.ets
890│   │   │   └── TaskViewModel.ets
891│   └── /resources
892│       ├── ...
893├─── ...
894```
895
896### Model层
897Model层负责管理应用的数据及其业务逻辑,通常与后端或数据存储进行交互。在todolist应用中,Model层的主要职责是存储任务数据、加载任务列表,并提供数据操作的接口,而不直接涉及UI展示。
898
899- TaskModel: 单个任务的基本数据结构,包含任务名称和完成状态。
900
901```ts
902// src/main/ets/model/TaskModel.ets
903
904export default class TaskModel {
905  taskName: string = 'Todo';
906  isFinish: boolean = false;
907}
908```
909
910- TaskListModel: 任务的集合,提供从本地加载任务数据的功能。
911```ts
912// src/main/ets/model/TaskListModel.ets
913
914import { common } from '@kit.AbilityKit';
915import util from '@ohos.util';
916import TaskModel from'./TaskModel';
917
918export default class TaskListModel {
919  tasks: TaskModel[] = [];
920
921  constructor(tasks: TaskModel[]) {
922    this.tasks = tasks;
923  }
924
925  async loadTasks(context: common.UIAbilityContext){
926    let getJson = await context.resourceManager.getRawFileContent('defaultTasks.json');
927    let textDecoderOptions: util.TextDecoderOptions = { ignoreBOM : true };
928    let textDecoder = util.TextDecoder.create('utf-8',textDecoderOptions);
929    let result = textDecoder.decodeToString(getJson);
930    this.tasks =JSON.parse(result).map((task: TaskModel)=>{
931      let newTask = new TaskModel();
932      newTask.taskName = task.taskName;
933      newTask.isFinish = task.isFinish;
934      return newTask;
935    });
936  }
937}
938```
939
940### ViewModel层
941
942ViewModel层负责管理UI状态和业务逻辑,扮演Model和View之间的桥梁角色。在ViewModel中,负责监控Model数据的变化,处理应用的逻辑,并将数据同步到View层,从而实现UI的自动更新。ViewModel的使用实现了数据与视图的解耦,提高了代码的可读性和可维护性。
943
944- TaskViewModel:封装单个任务的数据和状态变更逻辑,通过状态装饰器监控数据的变化。
945
946```ts
947// src/main/ets/viewmodel/TaskViewModel.ets
948
949import TaskModel from '../model/TaskModel';
950
951@ObservedV2
952export default class TaskViewModel {
953  @Trace taskName: string = 'Todo';
954  @Trace isFinish: boolean = false;
955
956  updateTask(task: TaskModel) {
957    this.taskName = task.taskName;
958    this.isFinish = task.isFinish;
959  }
960
961  updateIsFinish(): void {
962    this.isFinish = !this.isFinish;
963  }
964}
965```
966
967- TaskListViewModel: 封装了任务列表以及管理功能,包括加载任务、批量更新任务状态,以及添加和删除任务。
968
969```ts
970// src/main/ets/viewmodel/TaskListViewModel.ets
971
972import { common } from '@kit.AbilityKit';
973import { Type } from '@kit.ArkUI';
974import TaskListModel from '../model/TaskListModel';
975import TaskViewModel from'./TaskViewModel';
976
977@ObservedV2
978export default class TaskListViewModel {
979  @Type(TaskViewModel)
980  @Trace tasks: TaskViewModel[] = [];
981
982  async loadTasks(context: common.UIAbilityContext) {
983    let taskList = new TaskListModel([]);
984    await taskList.loadTasks(context)
985    for(let task of taskList.tasks){
986      let taskViewModel = new TaskViewModel();
987      taskViewModel.updateTask(task)
988      this.tasks.push(taskViewModel)
989    }
990  }
991
992  finishAll(ifFinish: boolean): void {
993    for(let task of this.tasks){
994      task.isFinish = ifFinish;
995    }
996  }
997
998  addTask(newTask: TaskViewModel): void {
999    this.tasks.push(newTask);
1000  }
1001
1002  removeTask(removedTask: TaskViewModel): void {
1003    this.tasks.splice(this.tasks.indexOf(removedTask), 1)
1004  }
1005}
1006```
1007
1008### View层
1009
1010View层负责应用程序的UI展示和与用户的交互。它只关注如何渲染用户界面和展示数据,不包含业务逻辑。所有的数据状态和逻辑都来自ViewModel层,View层通过接收ViewModel传递的状态数据进行渲染,确保视图和数据分离。
1011
1012- TitleView:负责展示应用的标题和未完成任务的统计信息。
1013
1014```ts
1015// src/main/ets/view/TitleView.ets
1016
1017@ComponentV2
1018export default struct TitleView {
1019  @Param tasksUnfinished: number = 0;
1020
1021  build() {
1022    Column() {
1023      Text('待办')
1024        .fontSize(40)
1025        .margin(10)
1026      Text(`未完成任务:${this.tasksUnfinished}`)
1027        .margin({ left: 10, bottom: 10 })
1028    }
1029  }
1030}
1031```
1032
1033- ListView:负责展示任务列表,并根据Setting中的设置筛选是否显示已完成的任务。它依赖于TaskListViewModel来获取任务数据,并通过TaskItem组件进行渲染,包括任务的名称、完成状态以及删除按钮。通过TaskViewModel和TaskListViewModel实现用户的交互,如切换任务完成状态和删除任务。
1034
1035```ts
1036// src/main/ets/view/ListView.ets
1037
1038import TaskViewModel from '../viewmodel/TaskViewModel';
1039import TaskListViewModel from '../viewmodel/TaskListViewModel';
1040import { Setting } from '../pages/SettingPage';
1041import { ActionButton } from './BottomView';
1042
1043@ComponentV2
1044struct TaskItem {
1045  @Param task: TaskViewModel = new TaskViewModel();
1046  @Event deleteTask: () => void = () => {};
1047  @Monitor('task.isFinish')
1048  onTaskFinished(mon: IMonitor) {
1049    console.log('任务' + this.task.taskName + '的完成状态从' + mon.value()?.before + '变为了' + mon.value()?.now);
1050  }
1051
1052  build() {
1053    Row() {
1054      // 请开发者自行在src/main/resources/base/media路径下添加finished.pngunfinished.png两张图片,否则运行时会因资源缺失而报错
1055      Image(this.task.isFinish ? $r('app.media.finished') : $r('app.media.unfinished'))
1056        .width(28)
1057        .height(28)
1058        .margin({ left: 15, right: 10 })
1059      Text(this.task.taskName)
1060        .decoration({ type: this.task.isFinish ? TextDecorationType.LineThrough : TextDecorationType.None })
1061        .fontSize(18)
1062      ActionButton('删除', () => this.deleteTask());
1063    }
1064    .height('7%')
1065    .width('90%')
1066    .backgroundColor('#90f1f3f5')
1067    .borderRadius(25)
1068    .onClick(() => this.task.updateIsFinish())
1069  }
1070}
1071
1072@ComponentV2
1073export default struct ListView {
1074  @Param taskList: TaskListViewModel = new TaskListViewModel();
1075  @Param setting: Setting = new Setting();
1076
1077  build() {
1078    Repeat<TaskViewModel>(this.taskList.tasks.filter(task => this.setting.showCompletedTask || !task.isFinish))
1079      .each((obj: RepeatItem<TaskViewModel>) => {
1080        TaskItem({
1081          task: obj.item,
1082          deleteTask: () => this.taskList.removeTask(obj.item)
1083        }).margin(5)
1084      })
1085  }
1086}
1087```
1088
1089- BottomView:负责提供与任务操作相关的按钮和输入框,如"全部完成"、"全部未完成","设置"三个按钮,以及添加新任务的输入框。点击"全部完成"和"全部未完成"时,通过TaskListViewModel更改所有任务的状态。点击"设置"按钮时,会导航到SettingAbility的设置页面。添加新任务时,通过TaskListViewModel新增任务到任务列表中。
1090
1091```ts
1092// src/main/ets/view/BottomView.ets
1093
1094import { common, Want } from '@kit.AbilityKit';
1095import TaskViewModel from '../viewmodel/TaskViewModel';
1096import TaskListViewModel from '../viewmodel/TaskListViewModel';
1097
1098@Builder export function ActionButton(text: string, onClick:() => void) {
1099  Button(text, { buttonStyle: ButtonStyleMode.NORMAL })
1100    .onClick(onClick)
1101    .margin({ left: 10, right: 10, top: 5, bottom: 5 })
1102}
1103
1104@ComponentV2
1105export default struct BottomView {
1106  @Param taskList: TaskListViewModel = new TaskListViewModel();
1107  @Local newTaskName: string = '';
1108  private context = getContext() as common.UIAbilityContext;
1109
1110  build() {
1111    Column() {
1112      Row() {
1113        ActionButton('全部完成', (): void => this.taskList.finishAll(true))
1114        ActionButton('全部未完成', (): void => this.taskList.finishAll(false))
1115        ActionButton('设置', (): void => {
1116          let wantInfo: Want = {
1117            deviceId: '', // deviceId为空表示本设备
1118            bundleName: 'com.example.mvvmv2_new',
1119            abilityName: 'SettingAbility',
1120          };
1121          this.context.startAbility(wantInfo);
1122        })
1123      }
1124      .margin({ top: 10, bottom: 5 })
1125      Row() {
1126        TextInput({ placeholder: '添加新任务', text: this.newTaskName })
1127          .onChange((value) => this.newTaskName = value)
1128          .width('70%')
1129        ActionButton('+', (): void => {
1130          let newTask = new TaskViewModel();
1131          newTask.taskName = this.newTaskName;
1132          this.taskList.addTask(newTask);
1133          this.newTaskName = '';
1134        })
1135      }
1136    }
1137  }
1138}
1139```
1140
1141- TodoListPage:todolist的主页面,包含以上的三个View组件(TitleView、ListView、BottomView),用于统一展示待办事项的各个部分,管理任务列表和用户设置。TodoListPage负责从ViewModel中获取数据,并将数据传递给各个子View组件进行渲染,通过PersistenceV2持久化任务数据,确保数据在应用重启后仍能保持一致。
1142
1143```ts
1144// src/main/ets/pages/TodoListPage.ets
1145
1146import TaskListViewModel from '../viewmodel/TaskListViewModel';
1147import { common } from '@kit.AbilityKit';
1148import { AppStorageV2, PersistenceV2 } from '@kit.ArkUI';
1149import { Setting } from '../pages/SettingPage';
1150import TitleView from '../view/TitleView';
1151import ListView from '../view/ListView';
1152import BottomView from '../view/BottomView';
1153
1154@Entry
1155@ComponentV2
1156struct TodoList {
1157  @Local taskList: TaskListViewModel = PersistenceV2.connect(TaskListViewModel, 'TaskList', () => new TaskListViewModel())!;
1158  @Local setting: Setting = AppStorageV2.connect(Setting, 'Setting', () => new Setting())!;
1159  private context = getContext(this) as common.UIAbilityContext;
1160
1161  async aboutToAppear() {
1162    this.taskList = PersistenceV2.connect(TaskListViewModel, 'TaskList', () => new TaskListViewModel())!;
1163    if (this.taskList.tasks.length == 0) {
1164      await this.taskList.loadTasks(this.context);
1165    }
1166  }
1167
1168  @Computed
1169  get tasksUnfinished(): number {
1170    return this.taskList.tasks.filter(task => !task.isFinish).length;
1171  }
1172
1173  build() {
1174    Column() {
1175      TitleView({ tasksUnfinished: this.tasksUnfinished })
1176      ListView({ taskList: this.taskList, setting: this.setting });
1177      BottomView({ taskList: this.taskList });
1178    }
1179    .height('100%')
1180    .width('100%')
1181    .alignItems(HorizontalAlign.Start)
1182    .margin({ left: 15 })
1183  }
1184}
1185```
1186
1187- SettingPage:设置页面,负责管理是否显示已完成任务的设置。通过\@AppStorageV2应用全局存储用户的设置,用户通过Toggle开关切换showCompletedTask状态。
1188
1189```ts
1190// src/main/ets/pages/SettingPage.ets
1191
1192import { AppStorageV2 } from '@kit.ArkUI';
1193import { common } from '@kit.AbilityKit';
1194
1195@ObservedV2
1196export class Setting {
1197  @Trace showCompletedTask: boolean = true;
1198}
1199
1200@Entry
1201@ComponentV2
1202struct SettingPage {
1203  @Local setting: Setting = AppStorageV2.connect(Setting, 'Setting', () => new Setting())!;
1204  private context = getContext(this) as common.UIAbilityContext;
1205
1206  build(){
1207    Column(){
1208      Text('设置')
1209        .fontSize(40)
1210        .margin({ bottom: 10 })
1211      Row() {
1212        Text('显示已完成任务');
1213        Toggle({ type: ToggleType.Switch, isOn:this.setting.showCompletedTask })
1214          .onChange((isOn) => {
1215            this.setting.showCompletedTask = isOn;
1216          })
1217      }
1218      Button('返回待办')
1219        .onClick(()=>this.context.terminateSelf())
1220        .margin({ top: 10 })
1221    }
1222    .alignItems(HorizontalAlign.Start)
1223  }
1224}
1225```
1226
1227## 总结
1228
1229本教程通过一个简单的待办事项应用示例,逐步引入了状态管理V2装饰器,并通过代码重构实现了MVVM架构。最终,将数据、逻辑和视图分层,使得代码结构更加清晰、易于维护。合理地使用Model、View和ViewModel,可以帮助开发者实现高效的数据与UI同步,简化开发流程并降低复杂性。希望通过这个示例,开发者能够更好地理解MVVM模式,并能将其灵活应用到自己项目的开发中,从而提高开发效率和代码质量。