Bootstrap

从0到1:用ArkTS搭建MVVM模板,解锁高效开发新姿势

一、引言

在当今快速发展的应用开发领域,构建高效、可维护的应用架构是开发者面临的关键挑战之一。随着应用规模的不断扩大和功能的日益复杂,传统的开发模式往往难以满足需求,这使得架构模式的选择变得至关重要。架构模式就像是建筑的蓝图,为应用的开发提供了清晰的结构和指导原则,能够显著提升开发效率、代码的可维护性以及应用的性能。

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 模板有更深入的理解和掌握,并在实际开发中不断探索和实践,充分发挥它们的优势,创造出更多优秀的应用。

;