在鸿蒙(HarmonyOS)原生应用开发中,MVVM(Model-View-ViewModel)是官方最推荐的架构模式。随着业务复杂度的提升,MVVM 能够通过清晰的职责划分和响应式数据绑定机制,有效解决代码耦合、状态管理混乱等问题。以下是 MVVM 在 ArkTS 中的最佳实践指南:

一、 核心概念与职责划分

MVVM 将应用分为三个核心部分,实现数据、视图与逻辑的彻底分离:

  1. Model(模型层):负责数据的获取、存储和处理(如网络请求、本地数据库操作),以及定义数据结构。它不依赖 View 和 ViewModel,仅作为纯粹的数据源。
  2. View(视图层):负责 UI 的渲染与交互事件传递。它不包含任何业务逻辑,仅通过绑定 ViewModel 提供的数据实现动态更新,并将用户的操作转发给 ViewModel。
  3. ViewModel(视图模型层):作为连接 Model 和 View 的桥梁。它接收 View 的交互事件,调用 Model 接口处理业务逻辑,并将处理后的数据暴露为可观测状态,供 View 绑定。

以一个常见的“获取并展示用户信息”的业务场景为例。在这个例子中,使用 TypeScript/ArkTS 风格的伪代码来展示这三个核心部分是如何协同工作的:

1. Model(模型层):纯粹的数据源

Model 层只关心“数据本身”和“数据从哪来”,完全不关心界面长什么样。它负责定义数据结构,并封装网络请求或数据库操作。

// 1. 定义数据结构(实体类)
export class User {
  id: string;
  name: string;
  age: number;
  
  constructor(id: string, name: string, age: number) {
    this.id = id;
    this.name = name;
    this.age = age;
  }
}

// 2. 数据获取服务(如网络请求或本地数据库)
export class UserService {
  async fetchUser(id: string): Promise<User> {
    // 模拟网络请求延迟
    await new Promise(resolve => setTimeout(resolve, 1000)); 
    return new User(id, '张三', 25);
  }
}

2. ViewModel(视图模型层):业务逻辑与状态管理

ViewModel 是承上启下的核心。它从 Model 获取原始数据,将其转换为 View 需要的状态(如加载状态、错误信息),并暴露方法供 View 调用。

import { User, UserService } from './UserService';

export class UserViewModel {
  private userService: UserService = new UserService();

  // 暴露给 View 绑定的可观察状态
  user: User | null = null;
  isLoading: boolean = false;
  errorMessage: string = '';

  // 暴露给 View 调用的业务命令(处理用户交互)
  async loadUser(id: string) {
    this.isLoading = true;
    this.errorMessage = '';
    try {
      const rawUser = await this.userService.fetchUser(id);
      this.user = rawUser; // 更新数据状态
    } catch (error) {
      this.errorMessage = '获取用户信息失败,请重试'; // 更新错误状态
    } finally {
      this.isLoading = false; // 更新加载状态
    }
  }
}

3. View(视图层):纯 UI 渲染与事件转发

View 层是一个“被动”的展示组件。它不包含任何 if/else 的业务判断,也不直接发起网络请求。它只做两件事:监听 ViewModel 的状态变化来刷新 UI,以及将用户的点击操作转发给 ViewModel。

@Component
export struct UserPage {
  // 引入 ViewModel
  private viewModel: UserViewModel = new UserViewModel();

  aboutToAppear() {
    // 页面初始化时,通知 ViewModel 去加载数据
    this.viewModel.loadUser('1001');
  }

  build() {
    Column({ space: 20 }) {
      // 纯 UI 渲染:根据 ViewModel 的状态展示不同的界面
      if (this.viewModel.isLoading) {
        LoadingProgress() // 显示加载中
      } else if (this.viewModel.errorMessage) {
        Text(this.viewModel.errorMessage).fontColor(Color.Red) // 显示错误
      } else if (this.viewModel.user) {
        Text(`姓名:${this.viewModel.user.name}`) // 显示数据
        Text(`年龄:${this.viewModel.user.age}`)
      }

      // 事件转发:将用户的点击操作交给 ViewModel 处理
      Button('重新加载').onClick(() => {
        this.viewModel.loadUser('1001');
      })
    }
    .padding(20)
  }
}

解析总结

在这个例子中,可以清晰地看到 MVVM 的解耦优势:

  • Model 完全独立:如果以后需要把数据源从“网络请求”换成“本地 SQLite 数据库”,你只需要修改 UserService,ViewModel 和 View 完全不需要改动。
  • View 极其轻量:View 里面没有任何业务逻辑,只有纯粹的 UI 布局。如果以后要重新设计界面样式,也不会影响到数据加载的逻辑。
  • ViewModel 专注状态:它完美地处理了“加载中”、“成功”、“失败”这三种 UI 状态的切换,View 只需要“傻瓜式”地绑定这些状态即可。

二、 工程目录结构最佳实践

为了让项目结构清晰、易于团队协作,建议采用标准化的分层目录组织方案:

src/
 ├── model/          // 数据模型定义与数据获取服务
 │   └── User.ts
 ├── viewmodel/      // 视图模型实现(业务逻辑与状态管理)
 │   └── UserViewModel.ts
 ├── view/           // UI 组件与页面
 │   └── UserPage.ets
 ├── resources/      // 静态资源
 └── entryability/   // 应用入口

三、 状态管理策略与响应式绑定

在 ArkTS 中,状态管理模块天然起到了 ViewModel 的作用。通过合理使用装饰器,可以实现数据变化自动触发 UI 更新(无需手动 findView/updateView):

  • @State:管理组件内部的私有状态(如动画、弹窗开关)。
  • @Link:实现父子组件之间的双向绑定,传递的是对父组件状态的“引用”而非副本,保证单一数据源。
  • @Observed 与 @ObjectLink:当数据变得复杂(如跨层级对象引用)时,使用 @Observed 标记 class,配合 @ObjectLink 监控复杂对象的变化,这是实现 MVVM 架构的核心机制。
1. @State 实例:组件内部私有状态

场景:控制一个密码输入框的“明文/密文”切换。这个状态纯粹属于当前 UI 组件,不需要与外界交互。

@Component
struct PasswordInput {
  // 1. 使用 @State 管理组件私有的 UI 状态
  @State isVisible: boolean = false;
  @State password: string = '';

  build() {
    Row({ space: 10 }) {
      TextInput({ text: this.password, type: this.isVisible ? InputType.Normal : InputType.Password })
        .onChange((value: string) => {
          this.password = value;
        })
      
      // 2. 状态变化自动触发 UI 更新,无需手动操作 DOM
      Button(this.isVisible ? '隐藏' : '显示')
        .onClick(() => {
          this.isVisible = !this.isVisible; // 修改状态,UI 自动刷新
        })
    }
  }
}
2. @Link 实例:父子组件双向同步

场景:父组件控制一个全局开关,子组件是一个开关按钮。点击子组件的按钮,父组件的状态也会同步改变。

// 父组件
@Entry
@Component
struct ParentComponent {
  @State isDarkMode: boolean = false;

  build() {
    Column({ space: 20 }) {
      Text(`当前主题: ${this.isDarkMode ? '暗黑' : '明亮'}`)
      // 使用 $ 操作符传递状态变量的引用
      ThemeSwitch({ switchState: $isDarkMode }) 
    }
  }
}

// 子组件
@Component
struct ThemeSwitch {
  // 使用 @Link 接收父组件的引用,实现双向绑定
  @Link switchState: boolean;

  build() {
    Toggle({ type: ToggleType.Switch, isOn: this.switchState })
      .onChange((isOn: boolean) => {
        this.switchState = isOn; // 子组件修改,父组件同步更新
      })
  }
}
3. @Observed 与 @ObjectLink 实例:MVVM 复杂数据驱动

场景:MVVM 架构下的用户信息展示。ViewModel 作为一个复杂的嵌套对象,View 层需要监听其内部属性的变化。

// 1. Model/ViewModel 层:使用 @Observed 赋予类被深度观察的能力
@Observed
class UserProfile {
  name: string;
  age: number;
  constructor(name: string, age: number) {
    this.name = name;
    this.age = age;
  }
}

class UserViewModel {
  profile: UserProfile = new UserProfile('鸿蒙开发者', 25);
  
  updateAge(newAge: number) {
    this.profile.age = newAge; // 修改深层属性
  }
}

// 2. View 层:使用 @ObjectLink 接收复杂对象
@Component
struct UserCard {
  // @ObjectLink 必须接收被 @Observed 装饰的类实例
  @ObjectLink profile: UserProfile;

  build() {
    Column({ space: 10 }) {
      Text(`姓名: ${this.profile.name}`)
      Text(`年龄: ${this.profile.age}`)
    }
    .padding(20)
    .border({ width: 1, color: '#ccc' })
  }
}

// 3. 页面入口:持有 ViewModel 并传递给子组件
@Entry
@Component
struct UserPage {
  // 父组件持有 ViewModel 实例
  private viewModel: UserViewModel = new UserViewModel();

  build() {
    Column({ space: 20 }) {
      // 将 ViewModel 中的复杂对象传递给子组件
      UserCard({ profile: this.viewModel.profile })

      Button('年龄 +1').onClick(() => {
        // 通过 ViewModel 修改数据,UserCard 会自动刷新
        this.viewModel.updateAge(this.viewModel.profile.age + 1);
      })
    }
    .padding(20)
  }
}

四、 MVVM 实战

1、代码示例

以下是一个完整的用户信息加载与修改的 MVVM 实践示例:

// 1. Model层:数据模型与数据获取
@ObservedV2
class UserModel { 
  @Trace name: string;
  @Trace age: number;
  constructor(name: string, age: number) {
    this.name = name;
    this.age = age;
  }
}

class UserService {
  async getUserInfo(): Promise<UserModel> {
    return new Promise((resolve) => {
      setTimeout(() => resolve(new UserModel("鸿蒙开发者", 25)), 1000);
    });
  }
}

// 2. ViewModel层:处理业务逻辑,暴露可观测状态
class UserViewModel {
  private userService: UserService = new UserService();
  public user: UserModel = new UserModel("", 0);

  async loadUserInfo() {
    const user = await this.userService.getUserInfo();
    this.user.name = user.name;
    this.user.age = user.age;
  }

  changeAge(newAge: number) {
    this.user.age = newAge;
  }
}

// 3. View层:纯UI构建与交互处理
@ComponentV2
@Entry
struct UserPage {
  private viewModel: UserViewModel = new UserViewModel();

  async onCreate() {
    await this.viewModel.loadUserInfo();
  }

  build() {
    Column({ space: 20 }) {
      Text(`姓名:${this.viewModel.user.name}`);
      Text(`年龄:${this.viewModel.user.age}`);
      Button("年龄+1").onClick(() => {
        // 交互事件只调用 ViewModel 的方法,不直接修改状态
        this.viewModel.changeAge(this.viewModel.user.age + 1);
      });
    }
    .padding(20);
  }
}
2、 架构分层:从基础 MVVM 到 Controller 层

在真实项目中,如果将所有业务逻辑(如参数校验、路由跳转、异常处理)都塞入 ViewModel,会导致其变得臃肿。推荐引入 Controller 层,形成 View -> Controller -> ViewModel -> Model 的五层结构:

  • View:仅负责 UI 渲染和抛出交互事件。
  • Controller:负责流程编排(如登录时的参数校验、调用 VM 方法、处理结果并执行路由跳转)。
  • ViewModel:握有唯一的 UI 状态(单一数据源),对上暴露 Actions,对下调用 Repository。
  • Model / Repository:聚合数据来源(HTTP、RDB、Preferences),提供语义化方法。
3、 状态模型设计:统一 UI 状态容器

为了避免在 View 层到处写 if/else 判断 loading 或 error 状态,建议在 ViewModel 中定义统一的 UiState 模型,将状态收敛:

export type UiState<T> = {
  loading: boolean;
  data: T;
  error?: string;
}

ViewModel 对外只暴露不可变的状态快照(Snapshot),View 层通过订阅该状态来驱动 UI 渲染。这确保了状态的单一数据源,防止视图直接修改内部状态。

4、 拥抱 V2 响应式特性(@ObservedV2 & @Trace)

随着鸿蒙生态的发展,V2 版本的状态管理正在成为主流。为了让 ViewModel 中的属性变化直接驱动 UI 更新,推荐使用以下组合:

  • 使用 @ComponentV2 替代 @Component
  • 使用 @ObservedV2 标记 ViewModel 类。
  • 使用 @Trace 标记 ViewModel 中需要触发 UI 更新的属性。
  • 在 View 中使用 @Local 声明组件持有的 ViewModel 实例。
    这种模式摒弃了 V1 时代需要在 View 层定义大量 @State 变量来代理 VM 状态的做法,实现了真正的数据驱动。

五、 复杂场景:MVI 模式的引入

对于状态极其复杂、交互频繁的页面(如聊天列表、地图轨迹、巡检表格),传统的 MVVM 可能会面临状态同步困难的问题。此时可以引入 MVI(Model-View-Intent) 模式:

  • 强调状态不可变单向数据流
  • 所有的用户操作(Intent)都转化为统一的事件流,ViewModel 根据事件流计算并输出全新的、不可变的状态(State),View 仅作为状态的纯函数渲染器。
  • 这种模式能极大降低复杂状态管理的认知负担,并天然支持状态回溯与调试。

六、 深度架构思考与进阶实践

  1. 分离关注点:将“UI状态”保留在组件内部(使用 @State),将“业务状态”沉淀到独立的、使用 @Observed 标记的 class(ViewModel)中。View 不应自己创建 ViewModel,而应通过依赖注入或 @Provide/@Consume 来消费它。
  2. 原子化更新与性能保障:合理拆分状态。将不同 UI 区域依赖的状态拆分到不同的 @State 或 ViewModel 属性中,框架会精准重绘依赖该状态的最小 UI 单元,避免全页刷新带来的性能损耗。
  3. 引入仓颉(Cangjie)进行性能重构:当 ViewModel 承载的业务逻辑极其复杂(如实时数据分析、图像处理)时,ArkTS 的 JIT 编译可能成为瓶颈。此时可采用“分层 VM”架构:ArkTS 作为“薄 VM 层”负责状态绑定,而将重度计算、异步流管理下沉到仓颉编写的“重 VM 层”(CoreService)。通过命令下发与状态推送(一次性推送完整的视图状态对象)进行跨语言通信,最大化计算性能并最小化跨界通信开销。
  4. 避免不必要的状态变量:状态变量的管理有开销。如果变量仅用于读取且没有修改操作,或者没有关联任何 UI 组件,不要使用 @State 或 @Observed 标记,直接使用普通变量即可。
  5. 最小化状态共享范围:在没有强烈业务需求时,按照状态需要共享的最小范围选择合适的装饰器。避免滥用 @Provide/@Consume 或 AppStorage,这会导致非必要的 UI 刷新。
  6. 使用临时变量替代状态变量:在连续修改状态变量时(如字符串拼接),先使用局部临时变量进行计算,最后一次性赋值给状态变量。这能减少 ArkUI 框架查询依赖和重绘组件的次数。
  7. View 层不碰副作用:严格遵守边界,View 层只做展示和事件转发,绝不直接进行网络请求、数据库读写或复杂的业务计算。
Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐