MVVM 架构模式实现——构建企业级鸿蒙应用的骨架与灵魂
文章目录

每日一句正能量
隧道尽头一定有光,即便你此刻看不到出口,也许再走十步,光就在那里等你。
人在隧道中最绝望的不是黑暗,而是不知道隧道有多长。光的存在是客观的,只是被暂时的弯道遮挡了。把宏大的“尽头”拆解为具体的“十步”,这种微小的坚持,往往是走出至暗时刻的唯一方法。
一、引言:从「写代码」到「建架构」的跃迁
在前四篇文章中,我们系统掌握了 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 核心特征
- 双向绑定:ViewModel 状态变化自动驱动 View 刷新;View 事件通过回调通知 ViewModel 处理。
- 职责分离:View 只管 UI,ViewModel 只管业务,Model 只管数据,三层独立演进。
- 可测试性:ViewModel 不依赖 UI 框架,可独立单元测试;Mock Model 层即可验证业务逻辑。
- 响应式驱动:利用 ArkUI 的
@Observed/@State实现自动刷新,开发者无需手动调用setState。
三、ArkUI 中的 MVVM 实现机制
3.1 数据流向

图2:MVVM 中数据流严格单向,ViewModel 状态变化通过 ArkUI 响应式系统自动驱动 View 刷新
完整数据流:
- 用户操作 → View 层捕获事件(
Button.onClick、TextInput.onChange); - 事件回调 → View 调用 ViewModel 的业务方法(
handleClick、updateState); - 数据请求 → ViewModel 调用 Model 层获取原始数据(
fetchData、saveToLocal); - 状态更新 → Model 返回数据后,ViewModel 更新
@Observed/@State状态; - 自动刷新 → 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[]> { ... }
}
九、最佳实践总结
- 严格分层:View 不碰业务,ViewModel 不碰 UI,Model 不碰状态;
- 状态集中:页面级状态统一放在 ViewModel,避免分散在多个组件;
- 计算属性:数据转换用
get方法,避免在 View 中写逻辑表达式; - 事件回调:View 通过回调通知 ViewModel,不直接调用 ViewModel 的私有方法;
- 异步规范:所有 IO 操作(网络、存储)必须异步,处理 loading/error 状态;
- 测试优先:核心业务逻辑先写单元测试,再写 UI 代码;
- 资源管理:在
aboutToDisappear中清理定时器、取消网络请求。
十、结语
MVVM 不是一套复杂的框架,而是一种「让代码各归其位」的设计哲学。在 ArkUI 中,借助 @Observed、@State、@ObjectLink 等响应式装饰器,MVVM 的实现变得异常自然——你不需要引入额外的库,不需要写繁琐的绑定代码,只需要遵循「数据驱动 UI」的思维模式,框架会自动帮你完成剩下的工作。
掌握 MVVM,意味着你不再是一个「写 UI 的程序员」,而是一个「设计架构的工程师」。当你的代码能够经受住需求变更、团队协作和长期维护的考验时,你就真正掌握了企业级鸿蒙应用开发的精髓。
转载自:https://blog.csdn.net/u014727709/article/details/163509278
欢迎 👍点赞✍评论⭐收藏,欢迎指正
更多推荐



所有评论(0)