一、引言
在当今快速发展的应用开发领域,构建高效、可维护的应用架构是开发者面临的关键挑战之一。随着应用规模的不断扩大和功能的日益复杂,传统的开发模式往往难以满足需求,这使得架构模式的选择变得至关重要。架构模式就像是建筑的蓝图,为应用的开发提供了清晰的结构和指导原则,能够显著提升开发效率、代码的可维护性以及应用的性能。
MVVM(Model - View - ViewModel)作为一种经典的架构模式,在现代应用开发中占据着重要地位。它通过将应用分为 Model(模型)、View(视图)和 ViewModel(视图模型)三个核心部分,实现了数据、视图与逻辑的分离。这种分离不仅使得代码结构更加清晰,易于维护和扩展,还通过数据绑定机制,实现了数据与视图的自动同步,大大简化了开发过程,提高了开发效率。
而 ArkTS 作为华为鸿蒙系统应用开发的重要编程语言,具有强大的功能和特性。它基于 TypeScript 扩展而来,支持声明式 UI 编程、响应式编程以及组件化开发等,为开发者提供了更加高效、便捷的开发体验。将 MVVM 与 ArkTS 相结合,能够充分发挥两者的优势,为鸿蒙应用开发带来新的思路和方法。
在接下来的内容中,我们将深入探讨 MVVM 模式的原理和优势,详细介绍 ArkTS 语言的特性,以及如何使用 ArkTS 实现 MVVM 模板。同时,我们还会通过实际的代码示例,帮助大家更好地理解和掌握这一开发模式,为鸿蒙应用开发打下坚实的基础。
二、MVVM 架构模式解析
2.1 MVVM 核心概念
MVVM 模式将应用程序分为三个核心部分:Model(模型)、View(视图)和 ViewModel(视图模型)。这三个部分各司其职,协同工作,为构建高效、可维护的应用提供了坚实的基础。
- Model:Model 负责存储和管理应用的数据以及业务逻辑,是应用程序的数据基础。它不直接与用户界面交互,通常从后端接口获取数据,确保数据的一致性和完整性。例如,在一个电商应用中,商品的信息、用户的订单数据等都存储在 Model 中。这些数据可能来自于数据库、API 接口或者其他数据源。Model 通过提供数据访问方法,为 ViewModel 和 View 提供数据支持。
- View:View 负责用户界面展示数据并与用户交互,它不包含任何业务逻辑。View 通过绑定 ViewModel 层提供的数据来动态更新 UI,为用户呈现直观的操作界面。在上述电商应用中,商品列表页面、购物车页面等都是 View 的具体体现。View 通过 HTML、CSS 和 JavaScript 等技术,将数据以可视化的方式呈现给用户,并接收用户的输入和操作。
- ViewModel:ViewModel 是连接 Model 和 View 的桥梁,负责管理 UI 状态和交互逻辑。它监控 Model 数据的变化,通知 View 更新 UI,同时处理用户交互事件并转换为数据操作。在电商应用中,ViewModel 负责处理用户添加商品到购物车、修改商品数量、提交订单等操作。它从 Model 中获取数据,将数据转换为 View 可以使用的格式,并将 View 的用户交互事件转换为对 Model 的操作。
2.2 MVVM 工作原理
MVVM 的工作原理基于数据绑定和命令模式,实现了数据与视图的自动同步和用户交互的响应。
- 数据绑定:数据绑定是 MVVM 的核心机制,它实现了 View 和 ViewModel 之间的数据自动同步。当 ViewModel 中的数据发生变化时,View 会自动更新以反映这些变化;反之,当用户在 View 中进行操作导致数据变化时,ViewModel 也能及时感知并更新 Model 中的数据。在实际应用中,我们可以使用双向数据绑定来实现更加便捷的数据交互。例如,在一个表单输入框中,用户输入的数据会实时同步到 ViewModel 中的相应属性,同时 ViewModel 中属性的变化也会立即反映在输入框中。
- 命令模式:命令模式用于处理用户在 View 中的交互事件,如点击按钮、输入文本等。View 通过命令与 ViewModel 进行通信,将用户的操作转换为 ViewModel 中的方法调用。例如,在一个按钮点击事件中,我们可以将按钮的点击命令绑定到 ViewModel 中的一个方法,当用户点击按钮时,该方法会被自动调用,从而实现相应的业务逻辑。
2.3 MVVM 优势
MVVM 模式在应用开发中具有诸多优势,这些优势使得它成为现代应用开发的首选架构模式之一。
- 高可维护性:MVVM 通过将数据、视图和逻辑分离,使得代码结构更加清晰,易于维护和扩展。当业务逻辑发生变化时,只需修改 ViewModel 中的代码,而无需对 View 和 Model 进行大规模的改动。在一个复杂的电商应用中,如果需要添加新的商品属性或者修改商品展示方式,只需要在 ViewModel 中添加相应的逻辑和数据处理方法,而不会影响到 View 和 Model 的其他部分。
- 可测试性:由于 ViewModel 独立于 View 和 Model,它可以进行独立的单元测试,提高了代码的可测试性。开发人员可以通过编写测试用例来验证 ViewModel 中的业务逻辑是否正确,从而确保应用的质量。在测试 ViewModel 时,我们可以模拟各种输入情况,验证 ViewModel 的输出是否符合预期,这样可以大大提高代码的可靠性和稳定性。
- 低耦合度:View 和 Model 之间通过 ViewModel 进行通信,降低了它们之间的耦合度。这使得 View 和 Model 可以独立变化和扩展,互不影响。在电商应用中,如果需要更换 View 的展示风格或者修改 Model 的数据存储方式,只需要在 ViewModel 中进行相应的调整,而不会对其他部分造成影响。
三、ArkTS 语言基础速览
3.1 ArkTS 简介
ArkTS 是 HarmonyOS 优选的主力应用开发语言,它基于 TypeScript 扩展而来,为开发者提供了更强大的功能和更便捷的开发体验。
ArkTS 具有以下显著特点:
- 强类型特性:在编译时进行严格的类型检查,有助于在开发阶段尽早发现和修复错误,提高代码的稳定性和可靠性。这就好比在建造房屋时,提前检查建筑材料的质量和规格,确保房屋的结构稳固。在 ArkTS 中,如果将一个字符串类型的值赋给一个声明为数字类型的变量,编译器会立即报错,提示类型不匹配的问题。
- 支持装饰器:装饰器是一种特殊的声明,能够在不改变原类和使用继承的情况下,动态地扩展对象的功能。在 ArkTS 中,装饰器被广泛应用于组件定义、状态管理等方面,为开发者提供了更加灵活和高效的编程方式。例如,@Entry 装饰器用于将一个自定义组件标记为入口组件,使其成为应用的起始页面;@State 装饰器用于标记状态变量,当状态变量发生变化时,与之绑定的 UI 组件会自动更新。
- 声明式 UI 编程:允许开发者以声明的方式描述 UI 的结构和外观,而无需关注具体的 UI 绘制和渲染过程。通过简洁的语法和直观的表达方式,开发者可以更加专注于业务逻辑的实现,提高开发效率。在构建一个按钮组件时,开发者只需使用简单的代码描述按钮的文本、样式和点击事件等属性,而无需手动操作 DOM 元素来创建和更新按钮。
3.2 基本语法
ArkTS 的基本语法与 TypeScript 有很多相似之处,但也有一些独特的扩展和特性。以下是一些常见的基本语法:
- 变量声明:使用 let 或 const 关键字声明变量。let 声明的变量是可变的,而 const 声明的常量是不可变的。同时,ArkTS 支持类型推断,当变量被初始化时,编译器可以根据初始值自动推断变量的类型。例如:
let count: number = 10; // 显式指定类型 const message = "Hello, ArkTS!"; // 类型推断
- 函数定义:函数定义包括函数名、参数列表、返回类型和函数体。ArkTS 支持函数重载,允许定义多个同名函数,但参数列表或返回类型不同。例如:
function add(a: number, b: number): number { return a + b; } function greet(name: string): void { console.log(`Hello, ${name}`); }
- 控制语句:支持常见的控制语句,如 if - else、for、while、switch 等。这些控制语句用于控制程序的执行流程,实现条件判断和循环操作。例如:
let num = 5; if (num > 0) { console.log("Positive number"); } else if (num < 0) { console.log("Negative number"); } else { console.log("Zero"); } for (let i = 0; i < 5; i++) { console.log(i); } let i = 1; while (i <= 5) { console.log(i); i++; } let day: number = 3; switch (day) { case 1: console.log("星期一"); break; case 2: console.log("星期二"); break; case 3: console.log("星期三"); break; case 4: console.log("星期四"); break; case 5: console.log("星期五"); break; case 6: console.log("星期六"); break; case 7: console.log("星期日"); break; default: console.log("无效的日期"); }
-
3.3 与 MVVM 的契合点
ArkTS 的特性与 MVVM 架构模式高度契合,能够为 MVVM 开发带来诸多优势:
- 声明式 UI 与 View 层:ArkTS 的声明式 UI 编程风格使得 View 层的开发更加简洁和直观。开发者可以通过声明式的语法描述 UI 的结构和样式,而无需手动操作 DOM 元素。这种方式与 MVVM 中 View 层的职责相匹配,使得 View 层能够专注于数据的展示,而不需要处理复杂的 UI 更新逻辑。在一个列表页面中,开发者可以使用 ArkTS 的声明式语法轻松地创建列表组件,并通过数据绑定将 ViewModel 中的数据展示在列表中。当 ViewModel 中的数据发生变化时,View 层会自动更新,无需开发者手动干预。
- 状态管理与 ViewModel 层:ArkTS 提供了强大的状态管理机制,通过 @State、@Prop、@Link 等装饰器,能够方便地管理组件的状态和数据传递。在 MVVM 中,ViewModel 层负责管理 UI 状态和交互逻辑,ArkTS 的状态管理机制使得 ViewModel 层能够更好地实现这一职责。通过使用 @State 装饰器标记状态变量,开发者可以轻松地实现数据的响应式更新,当状态变量发生变化时,相关的 UI 组件会自动更新。同时,@Prop 和 @Link 装饰器用于父子组件之间的数据传递,实现了数据的单向和双向绑定,使得 ViewModel 层与 View 层之间的通信更加便捷和高效。
- 类型安全与代码质量:ArkTS 的强类型特性有助于提高代码的质量和可维护性。在 MVVM 开发中,确保数据的类型安全对于保证应用的稳定性和正确性至关重要。通过在编译时进行类型检查,ArkTS 能够及时发现类型错误,避免在运行时出现难以调试的问题。这使得开发者能够更加放心地编写代码,减少错误的发生,提高开发效率。
-
四、用 ArkTS 构建 MVVM 模板步骤
4.1 项目初始化
首先,确保你已经安装了 DevEco Studio 开发工具,它是华为官方提供的用于鸿蒙应用开发的集成开发环境,功能强大且易于使用。
打开 DevEco Studio,点击 “Create Project” 创建一个新项目。在项目创建向导中,选择 “Application” 应用开发类型,然后选择 “Empty Ability” 模板,这将为我们创建一个基础的项目结构,方便后续的开发。点击 “Next” 进入下一步配置。
在配置工程界面,设置项目的相关参数。Compile SDK 选择你所需的版本,这里建议选择较新的版本以获取更多的功能和优化。Model 选择 “Stage”,它是鸿蒙应用开发的一种模型,提供了更灵活和强大的应用开发能力。其他参数可以保持默认设置,然后点击 “Finish” 完成项目创建。
项目创建完成后,我们来了解一下项目的目录结构。在项目根目录下,有一个 “AppScope” 目录,它包含了应用的全局配置信息,如应用名称、版本号、设备配置等。“entry” 目录是 HarmonyOS 工程模块,编译构建后会生成一个 HAP 包,它是鸿蒙应用的发布包格式。
在 “entry/src/main” 目录下,有 “ets” 和 “resources” 两个主要目录。“ets” 目录用于存放 ArkTS 源码,其中 “entryability” 目录包含应用的入口文件,“pages” 目录则存放应用的各个页面组件。“resources” 目录用于存放应用所需的资源文件,如图片、字符串、样式等。
此外,还有一些配置文件,如 “build-profile.json5” 用于配置当前模块的编译信息,“hvigorfile.ts” 是模块级编译构建任务脚本,“module.json5” 文件用于配置模块的构建目标和签名信息。
4.2 Model 层构建
在 “ets” 目录下创建一个 “model” 文件夹,用于存放数据模型相关的代码。在 “model” 文件夹中,创建一个名为 “User.ets” 的数据模型类,用于模拟用户数据。
/** * 用户类,用于表示系统中的用户信息。 */ export class User { // 用户的唯一标识符 private id: number; // 用户的姓名 private name: string; // 用户的年龄 private age: number; /** * 构造函数,用于创建一个新的用户实例。 * @param id - 用户的唯一标识符。 * @param name - 用户的姓名。 * @param age - 用户的年龄。 */ constructor(id: number, name: string, age: number) { this.id = id; this.name = name; this.age = age; } /** * 获取用户的唯一标识符。 * @returns 用户的唯一标识符。 */ public getId(): number { return this.id; } /** * 获取用户的姓名。 * @returns 用户的姓名。 */ public getName(): string { return this.name; } /** * 获取用户的年龄。 * @returns 用户的年龄。 */ public getAge(): number { return this.age; } }
在上述代码中,我们定义了一个User类,它包含了id、name和age三个属性,分别表示用户的唯一标识、姓名和年龄。通过构造函数初始化这些属性,并提供了相应的访问器方法,以便在其他地方获取这些属性的值。
接下来,我们可以模拟从后端获取数据的操作。在 “service” 文件夹中创建一个名为 “UserService.ets” 的服务类,用于模拟数据获取。
// 导入 User 类,用于表示系统中的用户信息 import { User } from "./User"; /** * 用户服务类,用于处理与用户相关的业务逻辑。 */ export class UserService { /** * 异步获取用户信息。 * @returns 一个 Promise,解析为 User 对象。 */ public static async getUser(): Promise<User> { // 模拟异步从后端获取数据,这里直接返回一个固定的用户对象 return new User(1, "张三", 25); } }
在这个UserService类中,我们定义了一个静态方法getUser,它返回一个Promise对象,模拟异步从后端获取用户数据。在实际应用中,这里会通过网络请求从后端接口获取数据,然后将数据解析并返回。
4.3 ViewModel 层实现
在 “ets” 目录下创建一个 “viewmodel” 文件夹,用于存放视图模型相关的代码。在 “viewmodel” 文件夹中,创建一个名为 “UserViewModel.ts” 的视图模型类。
// 导入 User 类,用于表示系统中的用户信息 import { User } from '../model/User'; // 导入 UserService 类,用于处理与用户相关的业务逻辑 import { UserService } from '../service/UserService'; /** * 用户视图模型类,用于管理用户数据和业务逻辑。 */ @Observed export class UserViewModel { // 用户对象,初始值为 null private user: User | null = null; /** * 获取当前用户对象。 * @returns 当前用户对象,如果未获取到则返回 null。 */ public getUser(): User | null { return this.user; } /** * 异步获取用户数据并更新视图模型中的用户对象。 * @returns 一个 Promise,解析为空值。 */ public async fetchUser(): Promise<void> { try { // 调用 UserService 的 getUser 方法获取用户数据 this.user = await UserService.getUser(); } catch (error) { // 如果获取用户数据失败,打印错误信息到控制台 console.error('获取用户数据失败:', error); } } }
在上述代码中,我们使用了@observable装饰器将UserViewModel类标记为可观察的,这样当类中的属性发生变化时,相关的视图组件会自动更新。user属性用于存储用户数据,初始值为null。通过@action装饰器标记的fetchUser方法,用于从UserService获取用户数据,并将数据存储到user属性中。如果获取数据过程中发生错误,会在控制台输出错误信息。
4.4 View 层搭建
在 “ets/pages” 目录下,打开 “Index.ets” 文件,这是应用的主页面文件。我们将在这个文件中使用 ArkTS 的声明式语法构建用户界面,展示用户数据。
// 导入 UserViewModel 类,用于管理用户数据和业务逻辑 import { UserViewModel } from '../viewmodel/UserViewModel'; /** * 主页面组件,用于展示用户信息或获取用户数据的按钮。 */ @Entry @Component struct Index { // 创建 UserViewModel 实例,用于管理用户数据 private userViewModel: UserViewModel = new UserViewModel(); //姓名 @State private name: string = ""; //年龄 @State private age: number = 0; /** * 构建页面组件。 * @returns 页面组件的构建结果。 */ build() { Column() { // 如果已经获取到用户数据,则展示用户信息 Text(`姓名: ${this.name}`) .fontSize(24) .fontWeight(FontWeight.Bold); Text(`年龄: ${this.age}`) .fontSize(20); // 如果未获取到用户数据,则展示获取用户数据的按钮 Button('获取用户数据') .fontSize(20) .onClick(async () => { // 点击按钮时,异步获取用户数据 await this.userViewModel.fetchUser(); // 更新组件的状态,显示获取到的用户数据 this.name = this.userViewModel.getUser()!.getName(); this.age = this.userViewModel.getUser()!.getAge(); }); } // 设置组件的高度和宽度为 100%,并添加内边距 .height('100%') .width('100%') .padding(20); } }
在上述代码中,我们首先导入了UserViewModel类,然后在Index组件中创建了一个userViewModel实例。点击按钮时调用userViewModel.fetchUser方法获取用户数据。
通过以上步骤,我们就完成了一个使用 ArkTS 实现的 MVVM 模板。在这个模板中,Model 层负责提供数据,ViewModel 层负责管理数据和业务逻辑,View 层负责展示数据和处理用户交互。通过这种分层架构,使得代码结构更加清晰,易于维护和扩展。
五、示例代码实战
5.1 简单计数器应用
首先,我们在viewmodel文件夹中创建一个CounterViewModel.ets文件,用于管理计数器的逻辑。
/** * 计数器视图模型类,用于管理计数器数据和业务逻辑。 */ @Observed export class CounterViewModel { // 计数器的当前值,初始值为 0 private count: number = 0; /** * 获取当前计数器的值。 * @returns 当前计数器的值。 */ public getCount(): number { return this.count; } /** * 增加计数器的值。 */ public increment(): void { this.count++; } /** * 减少计数器的值。 */ public decrement(): void { this.count--; } }
CounterViewModel 类提供了一个简单的计数器功能,它可以被视图观察,并且可以通过 increment() 和 decrement() 方法来改变计数器的值。这种设计模式有助于实现视图和数据的分离,使得代码更加模块化和易于维护。
然后,在pages文件夹中的Index.ets文件中,使用这个CounterViewModel来构建计数器的用户界面。
// 导入 UserViewModel 类,用于管理用户数据和业务逻辑 import { CounterViewModel } from '../viewmodel/CounterViewModel'; import { UserViewModel } from '../viewmodel/UserViewModel'; /** * 主页面组件,用于展示用户信息或获取用户数据的按钮。 */ @Entry @Component struct Index { // 创建 UserViewModel 实例,用于管理用户数据 private userViewModel: UserViewModel = new UserViewModel(); // 创建 CounterViewModel 实例,用于管理计数器数据 private counterViewModel: CounterViewModel = new CounterViewModel(); //姓名 @State private name: string = ""; //年龄 @State private age: number = 0; //计数 @State private count: number = 0; /** * 构建页面组件。 * @returns 页面组件的构建结果。 */ build() { Column() { // 如果已经获取到用户数据,则展示用户信息 Text(`姓名: ${this.name}`) .fontSize(24) .fontWeight(FontWeight.Bold); Text(`年龄: ${this.age}`) .fontSize(20); // 如果未获取到用户数据,则展示获取用户数据的按钮 Button('获取用户数据') .fontSize(20) .onClick(async () => { // 点击按钮时,异步获取用户数据 await this.userViewModel.fetchUser(); // 更新组件的状态,显示获取到的用户数据 this.name = this.userViewModel.getUser()!.getName(); this.age = this.userViewModel.getUser()!.getAge(); }); Text(`计数器: ${this.count}`) .fontSize(24) .fontWeight(FontWeight.Bold); Row() { Button('+') .fontSize(20) .width(80) .height(40) .backgroundColor('#007DFF') .fontColor('#FFFFFF') .onClick(() => { this.counterViewModel.increment(); this.count = this.counterViewModel.getCount(); }); Button('-') .fontSize(20) .width(80) .height(40) .backgroundColor('#FF4D4F') .fontColor('#FFFFFF') .onClick(() => { this.counterViewModel.decrement(); this.count = this.counterViewModel.getCount(); }); } } // 设置组件的高度和宽度为 100%,并添加内边距 .height('100%') .width('100%') .padding(20); } }
在这个Index组件中,我们创建了一个counterViewModel实例,并在界面上显示计数器的值。通过点击 “+” 和 “-” 按钮,调用counterViewModel的increment和decrement方法,实现计数器的增加和减少功能。由于CounterViewModel被标记为可观察的,当count属性的值发生变化时,界面上显示的计数器值会自动更新。
5.2 数据列表展示
接下来,我们实现一个数据列表展示的功能,从 Model 层获取数据,在 View 层展示数据列表,并通过 ViewModel 层处理数据的增删改查。
在model文件夹中创建一个Todo.ets文件,定义待办事项的数据模型。
/** * 待办事项类,用于表示系统中的待办事项信息。 */ export class Todo { // 待办事项的唯一标识符 private id: number; // 待办事项的标题 private title: string; // 待办事项的完成状态 private completed: boolean; /** * 构造函数,用于创建一个新的待办事项实例。 * @param id - 待办事项的唯一标识符。 * @param title - 待办事项的标题。 * @param completed - 待办事项的完成状态。 */ constructor(id: number, title: string, completed: boolean) { this.id = id; this.title = title; this.completed = completed; } /** * 获取待办事项的唯一标识符。 * @returns 待办事项的唯一标识符。 */ public getId(): number { return this.id; } /** * 获取待办事项的标题。 * @returns 待办事项的标题。 */ public getTitle(): string { return this.title; } /** * 获取待办事项的完成状态。 * @returns 待办事项的完成状态。 */ public getCompleted(): boolean { return this.completed; } /** * 设置待办事项的完成状态。 * @param value - 待办事项的完成状态。 */ public setCompleted(value: boolean) { this.completed = value; } }
在这个Todo类中,定义了id、title和completed三个属性,分别表示待办事项的唯一标识、标题和完成状态。
接着,在viewmodel文件夹中创建一个TodoViewModel.ts文件,用于管理待办事项的逻辑。
// 导入 Todo 类,用于表示系统中的待办事项信息 import { Todo } from '../model/Todo'; /** * 待办事项视图模型类,用于管理待办事项数据和业务逻辑。 */ export class TodoViewModel { // 待办事项数组,初始值为空数组 private todos: Todo[] = []; // 更新回调函数,初始值为空函数 private updateCallback: () => void = () => { }; /** * 设置更新回调函数。 * @param callback - 要设置的更新回调函数。 */ setUpdateCallback(callback: () => void) { this.updateCallback = callback; } /** * 获取当前的待办事项数组。 * @returns 当前的待办事项数组。 */ getTodos(): Todo[] { return this.todos; } /** * 异步获取待办事项数据并更新视图模型中的待办事项数组。 * @returns 一个 Promise,解析为空值。 */ async fetchTodos() { // 模拟异步获取数据 this.todos = [ new Todo(1, '学习ArkTS', false), new Todo(2, '完成项目文档', false), new Todo(3, '参加会议', false) ]; // 通知更新 this.notifyUpdate(); } /** * 切换指定待办事项的完成状态。 * @param todo - 要切换完成状态的待办事项。 */ toggleTodoCompletion(todo: Todo) { const index = this.todos.findIndex(t => t.getId() === todo.getId()); if (index !== -1) { this.todos[index].setCompleted(!this.todos[index].getCompleted()); // 通知更新 this.notifyUpdate(); } } /** * 从待办事项数组中删除指定的待办事项。 * @param todo - 要删除的待办事项。 */ deleteTodo(todo: Todo) { this.todos = this.todos.filter(t => t.getId() !== todo.getId()); // 通知更新 this.notifyUpdate(); } /** * 添加一个新的待办事项到待办事项数组中。 * @param title - 新待办事项的标题。 */ addTodo(title: string) { const newTodo = new Todo(this.todos.length + 1, title, false); this.todos.push(newTodo); // 通知更新 this.notifyUpdate(); } /** * 通知更新。 */ private notifyUpdate() { if (this.updateCallback) { this.updateCallback(); } } }
TodoViewModel 类负责管理待办事项的数据和业务逻辑,包括获取、添加、删除和切换待办事项的完成状态。它通过调用 TodoService 来获取数据,并通过可观察的属性来通知视图更新
最后,在pages文件夹中的Index.ets文件中,使用TodoViewModel来展示待办事项列表。
// 导入 TodoViewModel 类,用于管理待办事项数据和业务逻辑 import { TodoViewModel } from '../viewmodel/TodoViewModel'; // 导入 prompt 模块,用于显示提示信息 import prompt from '@ohos.promptAction'; // 导入 Todo 类,用于表示系统中的待办事项信息 import { Todo } from '../model/Todo'; /** * 待办事项页面组件,用于显示和管理待办事项。 */ @Entry @Component struct TodoPage { // 待办事项视图模型实例 private todoViewModel: TodoViewModel = new TodoViewModel(); // 新待办事项的标题,初始值为空字符串 @State newTodoTitle: string = ''; // 待办事项数组,初始值为空数组 @State private todos: Todo[] = []; /** * 在组件即将显示时调用,设置更新回调函数。 */ aboutToAppear() { this.todoViewModel.setUpdateCallback(() => { this.todos = this.todoViewModel.getTodos(); }); } /** * 构建组件的 UI。 * @returns 组件的 UI 描述。 */ build() { Column() { // 如果待办事项数组为空,显示一个按钮用于获取待办事项 if (this.todos.length === 0) { Button('获取待办事项') .fontSize(20) .onClick(async () => { // 调用视图模型的 fetchTodos 方法获取待办事项数据 await this.todoViewModel.fetchTodos(); }); } else { // 如果待办事项数组不为空,显示一个列表用于显示待办事项 List() { // 使用 ForEach 组件遍历待办事项数组,为每个待办事项创建一个列表项 ForEach(this.todos, (todo: Todo) => { ListItem() { Row() { // 显示一个复选框,用于切换待办事项的完成状态 Checkbox() .onChange((isChecked: boolean) => { // 调用视图模型的 toggleTodoCompletion 方法切换待办事项的完成状态 this.todoViewModel.toggleTodoCompletion(todo); }); // 显示待办事项的标题 Text(todo.getTitle()) .fontSize(20) // 根据待办事项的完成状态设置文本的装饰线 .decoration({ type: todo.getCompleted() ? TextDecorationType.LineThrough : TextDecorationType.None }); // 显示一个删除按钮,用于删除待办事项 Button('删除') .fontSize(16) // 设置按钮的背景颜色 .backgroundColor('#FF4D4F') // 设置按钮的字体颜色 .fontColor('#FFFFFF') .onClick(() => { // 调用视图模型的 deleteTodo 方法删除待办事项 this.todoViewModel.deleteTodo(todo); }); } // 设置行的宽度为 100% .width('100%'); } // 设置列表项的高度为 60 .height(60); }); } // 设置列表的宽度为 100% .width('100%'); } // 显示一个输入框和一个按钮,用于添加新的待办事项 Row() { TextInput({ // 设置输入框的占位符文本 placeholder: '输入新的待办事项', // 设置输入框的文本内容 text: this.newTodoTitle, }) // 设置输入框的宽度为 70% .width('70%') .onChange((value: string) => { // 当输入框的内容发生变化时,更新 newTodoTitle 变量 this.newTodoTitle = value; }) Button('添加') .fontSize(16) // 设置按钮的宽度为 30% .width('30%') // 设置按钮的背景颜色 .backgroundColor('#007DFF') // 设置按钮的字体颜色 .fontColor('#FFFFFF') .onClick(() => { // 如果新待办事项的标题不为空,调用视图模型的 addTodo 方法添加新的待办事项 if (this.newTodoTitle.trim() !== '') { this.todoViewModel.addTodo(this.newTodoTitle); // 清空新待办事项的标题 this.newTodoTitle = ''; } else { // 如果新待办事项的标题为空,显示一个提示信息 prompt.showToast({ message: '请输入待办事项内容' }); } }); } // 设置行的上边距为 20 .margin({ top: 20 }); } // 设置列的高度为 100% .height('100%') // 设置列的宽度为 100% .width('100%') // 设置列的内边距为 20 .padding(20); } }
在这个Index组件中,创建了一个todoViewModel实例,并在界面上展示待办事项列表。如果待办事项列表为空,显示一个 “获取待办事项” 按钮,点击按钮时调用todoViewModel.fetchTodos方法获取数据。当待办事项列表不为空时,使用List和ForEach组件遍历待办事项数组,展示每个待办事项的复选框、标题和删除按钮。通过点击复选框,调用todoViewModel.toggleTodoCompletion方法切换待办事项的完成状态;点击删除按钮,调用todoViewModel.deleteTodo方法删除待办事项。在界面下方,提供一个输入框和添加按钮,用户可以输入新的待办事项标题,点击添加按钮时,调用todoViewModel.addTodo方法添加新的待办事项。由于TodoViewModel被标记为可观察的,当todos属性的值发生变化时,界面上的待办事项列表会自动更新。
六、常见问题与解决方案
6.1 状态管理问题
在使用 MVVM 架构结合 ArkTS 进行开发时,状态管理是一个关键环节,但也容易出现一些问题。
状态同步不及时是一个常见的问题。当 Model 中的数据发生变化时,ViewModel 可能无法及时感知到这些变化,从而导致 View 不能及时更新。这可能是由于状态变量没有正确标记为可观察的,或者在数据更新时没有触发相应的通知机制。在上述的计数器应用中,如果CounterViewModel中的count属性没有使用@State装饰器标记,那么当count的值发生变化时,界面上显示的计数器值就不会自动更新。为了解决这个问题,我们需要确保所有需要被观察的状态变量都正确使用了@Observed装饰器,并且在数据更新的方法上使用@State装饰器,以触发视图的更新。
不必要的 UI 刷新也是一个需要关注的问题。在某些情况下,当状态变量发生变化时,可能会导致整个页面的 UI 都进行刷新,即使只有部分组件需要更新。这会影响应用的性能,特别是在页面比较复杂、组件较多的情况下。在数据列表展示的示例中,如果我们在TodoViewModel中更新_todos数组时,没有进行合理的优化,可能会导致整个待办事项列表所在的页面都进行刷新,而实际上可能只是某个待办事项的完成状态发生了变化,只需要更新该待办事项对应的组件即可。为了避免这种情况,我们可以使用细粒度的状态管理,将状态变量的变化范围尽量缩小,只通知那些真正依赖于该状态变量的组件进行更新。同时,在更新状态变量时,尽量避免不必要的重新赋值操作,可以使用临时变量进行计算,最后再将结果赋值给状态变量,这样可以减少 UI 刷新的次数。
6.2 数据绑定异常
在数据绑定过程中,也可能会出现一些异常情况。
绑定失效是一个常见的问题。这可能是由于绑定语法错误、绑定路径不正确或者数据源发生了变化但没有正确更新绑定关系。在使用 ArkTS 进行数据绑定时,如果我们在Index.ets文件中绑定UserViewModel中的user属性时,语法写错或者绑定路径错误,就会导致数据无法正确显示在界面上。为了排查这个问题,我们需要仔细检查绑定语法和绑定路径是否正确,确保数据源的类型和绑定目标的类型一致。同时,我们可以使用调试工具,如 DevEco Studio 提供的调试功能,查看绑定过程中是否有错误信息输出。
数据更新不及时也是一个需要解决的问题。当 ViewModel 中的数据发生变化时,View 可能没有及时更新,这可能是由于数据绑定的更新机制出现了问题。在上述的示例中,如果我们在TodoViewModel中更新了todos数组,但界面上的待办事项列表没有及时更新,可能是因为数据绑定的更新机制没有正确触发。为了解决这个问题,我们可以检查数据绑定的更新策略,确保数据更新时能够及时通知到 View 进行更新。同时,我们可以使用@Link装饰器来实现双向数据绑定,这样当 View 中的数据发生变化时,也能够及时更新到 ViewModel 中。
七、总结与展望
7.1 回顾要点
在本次探索中,我们深入研究了使用 ArkTS 构建 MVVM 模板的过程。从项目初始化开始,我们精心搭建了项目的基础框架,为后续的开发工作奠定了坚实的基础。在 Model 层,我们定义了数据模型和服务类,实现了数据的存储和获取逻辑,为整个应用提供了数据支持。ViewModel 层则负责管理 UI 状态和交互逻辑,通过 @Observed和 @State装饰器,实现了数据的响应式更新和业务逻辑的处理,成为连接 Model 和 View 的关键桥梁。在 View 层,我们运用 ArkTS 的声明式语法,构建了简洁直观的用户界面,通过数据绑定和事件处理,实现了与 ViewModel 的高效交互,为用户提供了良好的使用体验。
通过简单计数器应用和数据列表展示的示例,我们更加深入地理解了 MVVM 架构在实际开发中的应用。在计数器应用中,我们清晰地看到了 ViewModel 如何管理状态,以及 View 如何根据状态的变化进行实时更新,实现了简单而高效的交互逻辑。而在数据列表展示的示例中,我们全面展示了从数据获取、展示到增删改查操作的完整流程,进一步验证了 MVVM 架构在处理复杂业务逻辑时的强大能力和优势。
同时,我们也关注到了开发过程中可能出现的常见问题,如状态管理问题和数据绑定异常,并深入探讨了相应的解决方案。在状态管理方面,我们强调了正确使用 @Observed和 @State装饰器的重要性,以确保状态的及时同步和避免不必要的 UI 刷新。在数据绑定方面,我们详细讲解了如何排查和解决绑定失效、数据更新不及时等问题,通过仔细检查绑定语法、路径和更新机制,确保了数据的正确传递和界面的及时更新。
7.2 未来发展
随着 HarmonyOS 的不断发展和普及,ArkTS 作为其重要的开发语言,将在未来的应用开发中扮演更加重要的角色。MVVM 架构模式也将持续演进,不断适应新的技术和需求。在未来,我们可以期待 ArkTS 和 MVVM 在以下几个方面取得更大的发展:
- 性能优化:随着应用的复杂性不断增加,对性能的要求也越来越高。未来,ArkTS 和 MVVM 将在性能优化方面不断努力,通过更高效的算法、数据结构和渲染机制,提升应用的响应速度和运行效率,为用户带来更加流畅的使用体验。
- 更多功能扩展:为了满足不同场景的开发需求,ArkTS 和 MVVM 可能会引入更多的功能和特性。支持更丰富的状态管理模式、更强大的组件库和更便捷的开发工具,将为开发者提供更多的选择和便利,加快应用开发的速度和质量。
- 跨平台应用开发:随着移动应用、Web 应用和桌面应用的融合趋势,跨平台应用开发将成为未来的重要发展方向。ArkTS 和 MVVM 有望在跨平台开发领域发挥更大的作用,通过一次编写、多平台运行的方式,降低开发成本,提高开发效率,为用户提供更加一致的体验。
-
希望读者能够通过本文对用 ArkTS 构建 MVVM 模板有更深入的理解和掌握,并在实际开发中不断探索和实践,充分发挥它们的优势,创造出更多优秀的应用。