在这里插入图片描述

每日一句正能量

隧道尽头一定有光,即便你此刻看不到出口,也许再走十步,光就在那里等你。
人在隧道中最绝望的不是黑暗,而是不知道隧道有多长。光的存在是客观的,只是被暂时的弯道遮挡了。把宏大的“尽头”拆解为具体的“十步”,这种微小的坚持,往往是走出至暗时刻的唯一方法。


一、引言:从「写代码」到「建架构」的跃迁

在前四篇文章中,我们系统掌握了 ArkUI 状态管理的全套技术栈——从 @Provide/@Consume 的跨层通信,到 @Observed/@ObjectLink 的嵌套对象监听,从性能优化的六大策略,到企业级的最佳实践规范。然而,技术点的掌握只是第一步,将这些技术点有机组织成一套可持续演进的架构,才是区分「初级开发者」与「架构师」的分水岭。

MVVM(Model-View-ViewModel)正是这样一套经过千锤百炼的架构模式。它通过「职责分离」与「响应式绑定」,将业务逻辑从 UI 代码中彻底解耦,使代码更易测试、更易维护、更易团队协作。本文将深入剖析 MVVM 在 ArkUI 中的落地实现,并通过一个完整的任务管理系统实战,带你从理论走向工程。


二、MVVM 核心概念:三层各司其职

2.1 架构分层

在这里插入图片描述

图1:MVVM 三层架构各司其职,数据流严格单向,ArkUI 的响应式系统实现 View 与 ViewModel 的自动绑定

层级 职责 ArkUI 对应 核心原则
View(视图层) UI 渲染、布局、用户交互事件绑定 @Entry / @Component 纯声明式,无业务逻辑
ViewModel(视图模型层) 业务逻辑处理、状态管理、数据转换 @Observed 数据类 不依赖 UI 框架,可独立测试
Model(数据层) 网络请求、本地存储、数据解析 http / Preferences 纯数据操作,无状态管理

2.2 核心特征

  1. 双向绑定:ViewModel 状态变化自动驱动 View 刷新;View 事件通过回调通知 ViewModel 处理。
  2. 职责分离:View 只管 UI,ViewModel 只管业务,Model 只管数据,三层独立演进。
  3. 可测试性:ViewModel 不依赖 UI 框架,可独立单元测试;Mock Model 层即可验证业务逻辑。
  4. 响应式驱动:利用 ArkUI 的 @Observed/@State 实现自动刷新,开发者无需手动调用 setState

三、ArkUI 中的 MVVM 实现机制

3.1 数据流向

在这里插入图片描述

图2:MVVM 中数据流严格单向,ViewModel 状态变化通过 ArkUI 响应式系统自动驱动 View 刷新

完整数据流:

  1. 用户操作 → View 层捕获事件(Button.onClickTextInput.onChange);
  2. 事件回调 → View 调用 ViewModel 的业务方法(handleClickupdateState);
  3. 数据请求 → ViewModel 调用 Model 层获取原始数据(fetchDatasaveToLocal);
  4. 状态更新 → Model 返回数据后,ViewModel 更新 @Observed / @State 状态;
  5. 自动刷新 → ArkUI 检测到状态变化,标记依赖组件为 Dirty,执行 Diff 计算,精准刷新 UI。

3.2 ArkUI 响应式刷新四步曲

步骤 机制 开发者操作
① 状态变更 ViewModel 修改 @Observed 属性或 @State 变量 this.tasks.push(newTask)
② 依赖追踪 框架检测到状态变化,标记依赖该状态的组件为 Dirty 自动,无需干预
③ Diff 计算 框架对比虚拟 DOM 差异,生成最小更新指令集 自动,无需干预
④ 精准刷新 仅更新变化的 UI 节点,未变化节点保持复用 自动,无需干预

四、实战:任务管理系统完整实现

以下是一个完整的任务管理系统,涵盖增删改查、状态筛选、本地持久化,严格遵循 MVVM 分层。

在这里插入图片描述

图3:任务管理系统按 MVVM 分层,Model 定义数据结构,Service 处理网络,ViewModel 管理状态,View 负责渲染

4.1 Model 层:纯数据定义

// models/TaskStatus.ets
export enum TaskStatus {
  Pending = 'pending',
  InProgress = 'in_progress',
  Completed = 'completed'
}

// models/TaskModel.ets
@Observed
export class TaskModel {
  id: string = '';
  title: string = '';
  description: string = '';
  status: TaskStatus = TaskStatus.Pending;
  priority: number = 0;  // 0-低 1-中 2-高
  createTime: number = 0;
  deadline: number = 0;

  constructor(id: string, title: string, description: string = '', 
              priority: number = 1, deadline: number = 0) {
    this.id = id;
    this.title = title;
    this.description = description;
    this.priority = priority;
    this.createTime = Date.now();
    this.deadline = deadline || this.createTime + 7 * 24 * 3600 * 1000;
  }

  // 计算属性:是否逾期
  get isOverdue(): boolean {
    return this.status !== TaskStatus.Completed && Date.now() > this.deadline;
  }

  // 计算属性:优先级文本
  get priorityText(): string {
    const map = ['低', '中', '高'];
    return map[this.priority] || '中';
  }
}

Model 层规范

  • 使用 @Observed 装饰,使属性变化可被框架监听;
  • 只定义数据结构和计算属性,不写任何业务逻辑
  • 提供完整构造函数,确保实例化时所有字段有默认值。

4.2 Service 层:数据持久化与网络

// services/TaskService.ets
import { TaskModel, TaskStatus } from '../models/TaskModel';
import { preferences } from '@kit.ArkData';

export class TaskService {
  private static readonly STORAGE_KEY = 'task_list';
  private static readonly PREF_NAME = 'task_pref';

  // 从本地存储加载任务列表
  static async loadTasks(): Promise<TaskModel[]> {
    try {
      const pref = preferences.getPreferencesSync(getContext(), { name: this.PREF_NAME });
      const jsonStr = pref.getSync(this.STORAGE_KEY, '[]') as string;
      const rawList = JSON.parse(jsonStr) as Array<Record<string, unknown>>;

      return rawList.map(item => new TaskModel(
        item.id as string,
        item.title as string,
        item.description as string,
        item.priority as number,
        item.deadline as number
      ));
    } catch (err) {
      console.error('加载任务失败:', err);
      return [];
    }
  }

  // 保存任务列表到本地存储
  static async saveTasks(tasks: TaskModel[]): Promise<void> {
    try {
      const pref = preferences.getPreferencesSync(getContext(), { name: this.PREF_NAME });
      const jsonStr = JSON.stringify(tasks.map(t => ({
        id: t.id,
        title: t.title,
        description: t.description,
        status: t.status,
        priority: t.priority,
        deadline: t.deadline,
      })));
      pref.putSync(this.STORAGE_KEY, jsonStr);
      await pref.flush();
    } catch (err) {
      console.error('保存任务失败:', err);
    }
  }

  // 模拟网络同步(实际项目中替换为真实 HTTP 请求)
  static async syncWithServer(tasks: TaskModel[]): Promise<TaskModel[]> {
    // 模拟网络延迟
    await new Promise(r => setTimeout(r, 500));
    // 模拟服务端返回(实际使用 http.request)
    return tasks;
  }
}

Service 层规范

  • 所有方法均为静态方法,无状态,无副作用;
  • 负责数据序列化/反序列化、网络请求、本地存储;
  • 返回纯数据对象,不持有任何 UI 状态。

4.3 ViewModel 层:业务逻辑与状态管理

// viewmodels/TaskViewModel.ets
import { TaskModel, TaskStatus } from '../models/TaskModel';
import { TaskService } from '../services/TaskService';

export enum FilterType {
  All = 'all',
  Pending = 'pending',
  InProgress = 'in_progress',
  Completed = 'completed',
  Overdue = 'overdue'
}

@Observed
export class TaskViewModel {
  // ===== 状态定义 =====
  tasks: TaskModel[] = [];
  filter: FilterType = FilterType.All;
  isLoading: boolean = false;
  errorMessage: string = '';

  // ===== 计算属性 =====
  get filteredTasks(): TaskModel[] {
    switch (this.filter) {
      case FilterType.Pending:
        return this.tasks.filter(t => t.status === TaskStatus.Pending);
      case FilterType.InProgress:
        return this.tasks.filter(t => t.status === TaskStatus.InProgress);
      case FilterType.Completed:
        return this.tasks.filter(t => t.status === TaskStatus.Completed);
      case FilterType.Overdue:
        return this.tasks.filter(t => t.isOverdue);
      default:
        return this.tasks;
    }
  }

  get taskStats(): { total: number; pending: number; completed: number } {
    return {
      total: this.tasks.length,
      pending: this.tasks.filter(t => t.status !== TaskStatus.Completed).length,
      completed: this.tasks.filter(t => t.status === TaskStatus.Completed).length,
    };
  }

  // ===== 业务方法 =====
  async loadTasks(): Promise<void> {
    this.isLoading = true;
    this.errorMessage = '';
    try {
      this.tasks = await TaskService.loadTasks();
    } catch (err) {
      this.errorMessage = '加载任务失败,请重试';
    } finally {
      this.isLoading = false;
    }
  }

  async addTask(title: string, description: string = '', priority: number = 1): Promise<void> {
    if (!title.trim()) return;

    const newTask = new TaskModel(
      `task_${Date.now()}`,
      title.trim(),
      description,
      priority
    );
    this.tasks.push(newTask);
    await this.persistTasks();
  }

  async updateTaskStatus(id: string, status: TaskStatus): Promise<void> {
    const task = this.tasks.find(t => t.id === id);
    if (task) {
      task.status = status;
      await this.persistTasks();
    }
  }

  async deleteTask(id: string): Promise<void> {
    const index = this.tasks.findIndex(t => t.id === id);
    if (index !== -1) {
      this.tasks.splice(index, 1);
      await this.persistTasks();
    }
  }

  setFilter(filter: FilterType): void {
    this.filter = filter;
  }

  private async persistTasks(): Promise<void> {
    await TaskService.saveTasks(this.tasks);
  }
}

ViewModel 层规范

  • 使用 @Observed 装饰,使状态变化可驱动 UI 刷新;
  • 持有 @State 状态变量,暴露计算属性(get 方法);
  • 所有业务方法均为异步,处理 loading/error 状态;
  • 绝不直接操作 UI 组件,通过状态变更间接驱动刷新。

4.4 View 层:纯声明式 UI

// pages/TaskPage.ets
import { TaskViewModel, FilterType } from '../viewmodels/TaskViewModel';
import { TaskModel, TaskStatus } from '../models/TaskModel';
import { TaskInput } from '../components/TaskInput';
import { TaskItem } from '../components/TaskItem';
import { TaskFilter } from '../components/TaskFilter';

@Entry
@Component
struct TaskPage {
  @State viewModel: TaskViewModel = new TaskViewModel();

  aboutToAppear(): void {
    this.viewModel.loadTasks();
  }

  build() {
    Column({ space: 0 }) {
      // 标题栏
      this.HeaderSection()

      // 统计栏
      this.StatsSection()

      // 筛选器
      TaskFilter({
        currentFilter: this.viewModel.filter,
        onFilterChange: (filter: FilterType) => this.viewModel.setFilter(filter)
      })

      // 任务输入
      TaskInput({
        onAdd: (title: string, desc: string, priority: number) => {
          this.viewModel.addTask(title, desc, priority);
        }
      })

      // 任务列表
      this.TaskListSection()
    }
    .width('100%')
    .height('100%')
    .backgroundColor('#F5F7FA')
  }

  @Builder
  HeaderSection() {
    Row() {
      Text('任务管理')
        .fontSize(20)
        .fontWeight(FontWeight.Bold)
      Blank()
      if (this.viewModel.isLoading) {
        LoadingProgress().width(24).height(24).color('#1976D2')
      }
    }
    .width('100%')
    .height(56)
    .padding({ left: 20, right: 20 })
    .backgroundColor(Color.White)
  }

  @Builder
  StatsSection() {
    Row({ space: 16 }) {
      this.StatCard('全部', this.viewModel.taskStats.total, '#1976D2');
      this.StatCard('待办', this.viewModel.taskStats.pending, '#F57C00');
      this.StatCard('已完成', this.viewModel.taskStats.completed, '#388E3C');
    }
    .width('100%')
    .padding(16)
    .justifyContent(FlexAlign.SpaceEvenly)
  }

  @Builder
  StatCard(label: string, count: number, color: ResourceColor) {
    Column({ space: 4 }) {
      Text(`${count}`).fontSize(24).fontWeight(FontWeight.Bold).fontColor(color)
      Text(label).fontSize(12).fontColor('#999')
    }
    .width('30%')
    .padding(12)
    .backgroundColor(Color.White)
    .borderRadius(12)
  }

  @Builder
  TaskListSection() {
    if (this.viewModel.errorMessage) {
      Text(this.viewModel.errorMessage)
        .fontSize(14)
        .fontColor('#FF4D4F')
        .padding(20)
    } else if (this.viewModel.filteredTasks.length === 0) {
      Text('暂无任务')
        .fontSize(16)
        .fontColor('#CCC')
        .padding(40)
    } else {
      List({ space: 12 }) {
        ForEach(this.viewModel.filteredTasks, (task: TaskModel) => {
          ListItem() {
            TaskItem({
              task: task,
              onStatusChange: (id: string, status: TaskStatus) => {
                this.viewModel.updateTaskStatus(id, status);
              },
              onDelete: (id: string) => {
                this.viewModel.deleteTask(id);
              }
            })
          }
        }, (task: TaskModel) => task.id)
      }
      .width('100%')
      .layoutWeight(1)
      .padding({ left: 16, right: 16 })
      .cachedCount(3)
    }
  }
}

4.5 子组件:TaskItem

// components/TaskItem.ets
import { TaskModel, TaskStatus } from '../models/TaskModel';

@Component
export struct TaskItem {
  @ObjectLink task: TaskModel;
  onStatusChange: (id: string, status: TaskStatus) => void = () => {};
  onDelete: (id: string) => void = () => {};

  private get statusColor(): ResourceColor {
    switch (this.task.status) {
      case TaskStatus.Completed: return '#388E3C';
      case TaskStatus.InProgress: return '#1976D2';
      default: return '#F57C00';
    }
  }

  private get statusText(): string {
    switch (this.task.status) {
      case TaskStatus.Completed: return '已完成';
      case TaskStatus.InProgress: return '进行中';
      default: return '待办';
    }
  }

  build() {
    Row({ space: 12 }) {
      // 状态指示器
      Column() {
        Circle({ width: 12, height: 12 })
          .fill(this.statusColor)
        Text(this.statusText)
          .fontSize(10)
          .fontColor(this.statusColor)
          .margin({ top: 4 })
      }
      .width(50)

      // 任务内容
      Column({ space: 4 }) {
        Text(this.task.title)
          .fontSize(16)
          .fontWeight(this.task.status === TaskStatus.Completed ? FontWeight.Normal : FontWeight.Bold)
          .fontColor(this.task.status === TaskStatus.Completed ? '#999' : '#333')
          .decoration({
            type: this.task.status === TaskStatus.Completed ? TextDecorationType.LineThrough : TextDecorationType.None
          })

        if (this.task.description) {
          Text(this.task.description)
            .fontSize(12)
            .fontColor('#999')
            .maxLines(1)
            .textOverflow({ overflow: TextOverflow.Ellipsis })
        }

        Row({ space: 8 }) {
          Text(`优先级: ${this.task.priorityText}`)
            .fontSize(10)
            .fontColor('#666')
            .backgroundColor('#F5F5F5')
            .padding({ left: 6, right: 6, top: 2, bottom: 2 })
            .borderRadius(4)

          if (this.task.isOverdue) {
            Text('已逾期')
              .fontSize(10)
              .fontColor('#FF4D4F')
              .backgroundColor('#FFEBEE')
              .padding({ left: 6, right: 6, top: 2, bottom: 2 })
              .borderRadius(4)
          }
        }
      }
      .layoutWeight(1)
      .alignItems(HorizontalAlign.Start)

      // 操作按钮
      Column({ space: 8 }) {
        if (this.task.status !== TaskStatus.Completed) {
          Button('完成')
            .fontSize(12)
            .height(28)
            .padding({ left: 12, right: 12 })
            .backgroundColor('#388E3C')
            .onClick(() => this.onStatusChange(this.task.id, TaskStatus.Completed))
        }

        Button('删除')
          .fontSize(12)
          .height(28)
          .padding({ left: 12, right: 12 })
          .backgroundColor('#FF4D4F')
          .onClick(() => this.onDelete(this.task.id))
      }
    }
    .width('100%')
    .padding(16)
    .backgroundColor(Color.White)
    .borderRadius(12)
    .border({ width: 1, color: this.task.isOverdue ? '#FFCDD2' : '#F0F0F0' })
  }
}

五、MVVM 与传统命令式开发的对比

在这里插入图片描述

图4:MVVM 通过职责分离和响应式驱动,解决了传统命令式开发的耦合、测试、维护三大痛点

对比维度 传统命令式开发 MVVM 架构
代码耦合度 高(数据逻辑UI混杂) 低(三层职责分离)
UI 更新方式 手动调用 setText/setImage 状态驱动自动刷新
单元测试难度 难(需模拟UI环境) 易(ViewModel 可独立测试)
可维护性 差(修改影响面大) 好(各层独立演进)
团队协作 冲突频繁 并行开发效率高
适用场景 小型 Demo / 原型 中大型商业应用

六、ViewModel 单元测试实战

MVVM 最大的优势之一就是可测试性。以下是对 TaskViewModel 的单元测试:

// test/TaskViewModel.test.ets
import { TaskViewModel, FilterType } from '../viewmodels/TaskViewModel';
import { TaskModel, TaskStatus } from '../models/TaskModel';

function assertEqual(actual: unknown, expected: unknown, message: string): void {
  if (actual !== expected) {
    throw new Error(`${message}: expected ${expected}, got ${actual}`);
  }
}

// 测试添加任务
function testAddTask(): void {
  const vm = new TaskViewModel();
  vm.addTask('测试任务', '描述', 2);

  assertEqual(vm.tasks.length, 1, '任务列表长度应为 1');
  assertEqual(vm.tasks[0].title, '测试任务', '任务标题应匹配');
  assertEqual(vm.tasks[0].priority, 2, '优先级应匹配');
  assertEqual(vm.taskStats.total, 1, '统计总数应更新');
  console.info('✅ testAddTask 通过');
}

// 测试筛选功能
function testFilterTasks(): void {
  const vm = new TaskViewModel();
  vm.tasks = [
    new TaskModel('1', '任务A', '', 1),
    new TaskModel('2', '任务B', '', 1),
  ];
  vm.tasks[1].status = TaskStatus.Completed;

  vm.setFilter(FilterType.Completed);
  assertEqual(vm.filteredTasks.length, 1, '已完成筛选应返回 1 条');
  assertEqual(vm.filteredTasks[0].title, '任务B', '筛选结果应匹配');
  console.info('✅ testFilterTasks 通过');
}

// 测试删除任务
function testDeleteTask(): void {
  const vm = new TaskViewModel();
  vm.tasks = [
    new TaskModel('1', '任务A'),
    new TaskModel('2', '任务B'),
  ];

  vm.deleteTask('1');
  assertEqual(vm.tasks.length, 1, '删除后应剩 1 条');
  assertEqual(vm.tasks[0].id, '2', '剩余任务 ID 应匹配');
  console.info('✅ testDeleteTask 通过');
}

// 运行所有测试
export function runAllTests(): void {
  try {
    testAddTask();
    testFilterTasks();
    testDeleteTask();
    console.info('🎉 所有测试通过!');
  } catch (err) {
    console.error('❌ 测试失败:', err.message);
  }
}

测试优势

  • 无需启动 UI 环境,纯逻辑测试;
  • 可 Mock Service 层,验证各种边界条件;
  • 测试用例即文档,新成员快速理解业务规则。

七、生命周期管理与资源释放

在 MVVM 中,ViewModel 的生命周期通常与页面绑定。ArkUI 提供了 aboutToAppear / aboutToDisappear 等生命周期钩子:

@Entry
@Component
struct TaskPage {
  @State viewModel: TaskViewModel = new TaskViewModel();
  private timerId: number = -1;

  aboutToAppear(): void {
    // 页面即将显示:加载数据
    this.viewModel.loadTasks();

    // 启动定时同步(每 30 秒同步一次)
    this.timerId = setInterval(() => {
      this.viewModel.syncWithServer();
    }, 30000);
  }

  aboutToDisappear(): void {
    // 页面即将销毁:清理资源
    if (this.timerId !== -1) {
      clearInterval(this.timerId);
      this.timerId = -1;
    }
    // 取消未完成的网络请求
    this.viewModel.cancelPendingRequests?.();
  }

  onPageShow(): void {
    // 页面显示时刷新数据
    this.viewModel.loadTasks();
  }

  build() {
    // ...
  }
}

八、常见踩坑与规避方案

坑一:ViewModel 中直接操作 UI

// ❌ 错误:ViewModel 中弹出 Toast
class BadViewModel {
  async loadData() {
    const data = await fetchData();
    promptAction.showToast({ message: '加载成功' });  // ViewModel 不应依赖 UI API!
  }
}

// ✅ 正确:通过状态暴露结果,由 View 决定如何展示
class GoodViewModel {
  @State toastMessage: string = '';
  async loadData() {
    const data = await fetchData();
    this.toastMessage = '加载成功';  // View 监听此状态显示 Toast
  }
}

坑二:View 层写业务逻辑

// ❌ 错误:View 中处理数据转换
build() {
  Text(this.tasks.filter(t => t.status === 'completed').length.toString())
}

// ✅ 正确:转换逻辑放在 ViewModel 的计算属性中
class TaskViewModel {
  get completedCount(): number {
    return this.tasks.filter(t => t.status === TaskStatus.Completed).length;
  }
}
build() {
  Text(`${this.viewModel.completedCount}`)
}

坑三:Model 层持有状态

// ❌ 错误:Service 中维护状态
class BadService {
  private tasks: TaskModel[] = [];  // Service 不应持有状态!
}

// ✅ 正确:Service 无状态,纯函数式
class GoodService {
  static async loadTasks(): Promise<TaskModel[]> { ... }
}

九、最佳实践总结

  1. 严格分层:View 不碰业务,ViewModel 不碰 UI,Model 不碰状态;
  2. 状态集中:页面级状态统一放在 ViewModel,避免分散在多个组件;
  3. 计算属性:数据转换用 get 方法,避免在 View 中写逻辑表达式;
  4. 事件回调:View 通过回调通知 ViewModel,不直接调用 ViewModel 的私有方法;
  5. 异步规范:所有 IO 操作(网络、存储)必须异步,处理 loading/error 状态;
  6. 测试优先:核心业务逻辑先写单元测试,再写 UI 代码;
  7. 资源管理:在 aboutToDisappear 中清理定时器、取消网络请求。

十、结语

MVVM 不是一套复杂的框架,而是一种「让代码各归其位」的设计哲学。在 ArkUI 中,借助 @Observed@State@ObjectLink 等响应式装饰器,MVVM 的实现变得异常自然——你不需要引入额外的库,不需要写繁琐的绑定代码,只需要遵循「数据驱动 UI」的思维模式,框架会自动帮你完成剩下的工作。

掌握 MVVM,意味着你不再是一个「写 UI 的程序员」,而是一个「设计架构的工程师」。当你的代码能够经受住需求变更、团队协作和长期维护的考验时,你就真正掌握了企业级鸿蒙应用开发的精髓。


转载自:https://blog.csdn.net/u014727709/article/details/163509278
欢迎 👍点赞✍评论⭐收藏,欢迎指正

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐