
MVVM+ArkUI-X:鸿蒙应用中“数据-视图”解耦的设计模式升级
引言
在传统鸿蒙应用开发中,我们常面临“数据-视图”强耦合的困境:UI组件直接操作数据,业务逻辑散落在各个页面,导致代码维护成本高、复用性差。随着ArkUI-X的普及,其声明式UI范式与状态管理能力,为MVVM(Model-View-ViewModel)模式的落地提供了天然土壤。本文将以“待办事项应用”为例,详解如何通过MVVM+ArkUI-X实现“数据-视图”的深度解耦,提升代码可维护性与开发效率。
一、传统开发的痛点:数据与视图的“纠缠”
1.1 传统模式的典型问题
在MVC或MVP模式中,视图(View)与数据(Model)的交互往往通过控制器(Controller)或Presenter直接操作UI组件,导致:
代码冗余:数据变更时需手动更新多个UI节点;
可测试性差:视图逻辑与业务逻辑交织,单元测试需模拟复杂UI环境;
跨端适配难:多端UI差异需重复编写逻辑,违反DRY原则。
1.2 MVVM的核心优势
MVVM通过引入ViewModel作为数据与视图的“桥梁”,实现:
数据驱动视图:视图仅负责渲染,数据变更自动同步到UI;
职责清晰分离:Model专注数据,ViewModel封装业务逻辑,View专注界面;
跨端友好:状态与逻辑可复用,多端仅需调整视图层布局。
二、ArkUI-X+MVVM的技术底座
ArkUI-X为MVVM提供了三大核心能力:
2.1 声明式UI
通过@Entry、@Component装饰器定义组件,使用Column、List等容器组件构建界面,状态变更时自动重渲染。
2.2 状态管理
@State:组件内部私有状态,变更时触发自身重渲染;
@Prop:父组件传递的状态,子组件不可修改;
@Link:双向绑定的状态,父组件与子组件共享同一数据源。
2.3 事件响应
通过onClick、onChange等事件绑定,将用户操作转换为ViewModel中的逻辑调用。
三、实战:用MVVM+ArkUI-X重构待办事项应用
3.1 场景需求
开发一个支持增删改查的待办事项应用,要求:
数据变更自动同步到UI;
业务逻辑(如数据校验、本地存储)封装在ViewModel;
视图层仅负责展示与交互。
3.2 步骤1:定义Model层(数据模型)
Model层负责数据的存储与传输,需保持纯粹性(无业务逻辑)。本例中,我们定义Todo数据类,并通过本地数据库实现持久化。
// model/Todo.ts
export interface Todo {
id: string; // 唯一标识
title: string; // 待办标题
completed: boolean; // 完成状态
createTime: number; // 创建时间戳
// model/TodoDatabase.ts(本地数据库封装)
import rdb from ‘@ohos.data.rdb’;
export class TodoDatabase {
private static rdbStore: rdb.RdbStore | null = null;
private static readonly DB_NAME = ‘todo.db’;
private static readonly TABLE_NAME = ‘todos’;
// 初始化数据库
static async init() {
if (this.rdbStore) return;
const config = {
name: this.DB_NAME,
securityLevel: rdb.SecurityLevel.S1
};
this.rdbStore = await rdb.getRdbStore(config);
// 创建表(若不存在)
const sql =
CREATE TABLE IF NOT EXISTS ${this.TABLE_NAME} (
id TEXT PRIMARY KEY,
title TEXT NOT NULL,
completed INTEGER NOT NULL,
createTime INTEGER NOT NULL
)
;
await this.rdbStore.executeSql(sql);
// 插入待办
static async insert(todo: Omit<Todo, ‘id’>): Promise<string> {
const id = Date.now().toString(); // 简单生成唯一ID
const sql = INSERT INTO ${this.TABLE_NAME} VALUES (?, ?, ?, ?);
await this.rdbStore!.executeSql(sql, [id, todo.title, todo.completed ? 1 : 0, todo.createTime]);
return id;
// 查询所有待办
static async queryAll(): Promise<Todo[]> {
const sql = SELECT * FROM ${this.TABLE_NAME};
const result = await this.rdbStore!.querySql(sql);
return result.rows.map(row => ({
id: row.getString(0)!,
title: row.getString(1)!,
completed: row.getInt(2) === 1,
createTime: row.getLong(3)
}));
// 更新完成状态
static async updateCompleted(id: string, completed: boolean): Promise<void> {
const sql = UPDATE ${this.TABLE_NAME} SET completed = ? WHERE id = ?;
await this.rdbStore!.executeSql(sql, [completed ? 1 : 0, id]);
// 删除待办
static async delete(id: string): Promise<void> {
const sql = DELETE FROM ${this.TABLE_NAME} WHERE id = ?;
await this.rdbStore!.executeSql(sql, [id]);
}
3.3 步骤2:实现ViewModel层(业务逻辑层)
ViewModel负责处理业务逻辑(如数据校验、本地存储调用),并通过状态(@State、@Link)暴露给视图层。本例中,TodoViewModel封装了待办的全生命周期管理。
// viewmodel/TodoViewModel.ts
import { Todo } from ‘…/model/Todo’;
import { TodoDatabase } from ‘…/model/TodoDatabase’;
export class TodoViewModel {
// 暴露给视图层的状态:待办列表
@State todos: Todo[] = [];
// 初始化:加载本地数据
async init() {
await TodoDatabase.init(); // 初始化数据库
this.loadTodos();
// 加载待办列表(从本地数据库)
async loadTodos() {
try {
this.todos = await TodoDatabase.queryAll();
catch (error) {
console.error('加载待办失败', error);
}
// 新增待办
async addTodo(title: string) {
if (!title.trim()) {
throw new Error(‘标题不能为空’);
const newTodo: Omit<Todo, ‘id’> = {
title,
completed: false,
createTime: Date.now()
};
await TodoDatabase.insert(newTodo);
await this.loadTodos(); // 重新加载列表以更新视图
// 切换完成状态
async toggleTodoCompleted(id: string, completed: boolean) {
await TodoDatabase.updateCompleted(id, completed);
await this.loadTodos(); // 同步更新视图
// 删除待办
async deleteTodo(id: string) {
await TodoDatabase.delete(id);
await this.loadTodos(); // 同步更新视图
}
3.4 步骤3:构建View层(声明式UI)
视图层通过ArkUI组件绑定ViewModel的状态,用户操作触发ViewModel的方法,实现“数据变更→自动更新视图”的闭环。
// entry/src/main/ets/pages/Index.ets
import router from ‘@ohos.router’;
import promptAction from ‘@ohos.promptAction’;
import { TodoViewModel } from ‘…/viewmodel/TodoViewModel’;
@Entry
@Component
struct IndexPage {
// 初始化ViewModel
private viewModel = new TodoViewModel();
aboutToAppear() {
this.viewModel.init(); // 页面加载时初始化数据
build() {
Column() {
// 标题栏
Row() {
Text('待办事项')
.fontSize(24)
.fontWeight(FontWeight.Bold)
Blank()
Button('同步') // 可选:手动触发数据同步(如未来扩展云同步)
.onClick(() => this.viewModel.loadTodos())
.width(‘100%’)
.height(60)
.padding({ left: 16, right: 16 })
// 输入区域
Row() {
TextInput({ placeholder: '请输入待办标题' })
.width('70%')
.onChange((value) => {
// 输入内容实时同步到ViewModel(可选优化)
})
Button('添加')
.onClick(() => {
this.viewModel.addTodo(this.inputValue); // 触发新增逻辑
})
.width(‘90%’)
.margin({ top: 20 })
.padding(10)
// 待办列表
List() {
ForEach(this.viewModel.todos, (todo: Todo) => {
ListItem() {
Row() {
Checkbox()
.checked(todo.completed)
.onChange((checked) => {
this.viewModel.toggleTodoCompleted(todo.id, checked); // 触发状态切换
})
Text(todo.title)
.fontSize(18)
.decoration({
type: todo.completed ? TextDecorationType.LineThrough : TextDecorationType.None
})
Blank()
Text(this.formatTime(todo.createTime))
.fontSize(14)
.fontColor('#999')
Button('删除')
.fontSize(12)
.onClick(() => {
this.viewModel.deleteTodo(todo.id); // 触发删除逻辑
})
.width(‘100%’)
.padding(10)
.borderRadius(8)
.backgroundColor('#F5F5F5')
})
.width(‘90%’)
.margin({ top: 20 })
.width(‘100%’)
.height('100%')
// 输入框引用(用于获取输入值)
@State inputValue: string = ‘’;
// 格式化时间戳
private formatTime(timestamp: number): string {
const date = new Date(timestamp);
return {date.getMonth() + 1}-{date.getDate()} {date.getHours()}:{date.getMinutes()};
}
3.5 关键设计解析
3.5.1 数据驱动视图
ViewModel中的@State todos状态变更时,ArkUI-X会自动触发IndexPage的重渲染,无需手动操作DOM;
输入框的onChange事件仅更新本地inputValue状态(未直接修改ViewModel),避免不必要的渲染。
3.5.2 业务逻辑封装
所有数据操作(增删改查、数据库交互)集中在TodoViewModel中,视图层仅负责触发方法;
未来扩展云同步时,只需在ViewModel中添加syncWithCloud()方法,视图层无需修改。
3.5.3 跨端适配优势
若需将应用迁移到平板或智慧屏,仅需调整IndexPage的布局(如Column改为Row排列),ViewModel与Model层完全复用;
多端共享同一套业务逻辑,避免重复开发。
四、MVVM+ArkUI-X的进阶优化
4.1 状态共享与跨组件通信
通过@Prop和@Link实现父子组件状态共享,避免“状态提升”导致的层级冗余:
// 子组件TodoItem.ets
@Component
export struct TodoItem {
@Prop todo: Todo; // 父组件传递的状态(只读)
@Link viewModel: TodoViewModel; // 双向绑定的ViewModel
build() {
Row() {
Checkbox()
.checked(this.todo.completed)
.onChange((checked) => {
this.viewModel.toggleTodoCompleted(this.todo.id, checked); // 直接调用ViewModel方法
})
Text(this.todo.title)
.decoration({ type: this.todo.completed ? TextDecorationType.LineThrough : TextDecorationType.None })
}
// 父组件IndexPage中使用
List() {
ForEach(this.viewModel.todos, (todo) => {
ListItem() {
TodoItem({ todo, viewModel: $viewModel }) // 传递@Link的ViewModel
})
4.2 异步操作的优雅处理
使用async/await结合try/catch封装异步逻辑,避免回调地狱:
// ViewModel中优化后的新增逻辑
async addTodo(title: string) {
try {
if (!title.trim()) throw new Error(‘标题不能为空’);
const newTodo: Omit<Todo, 'id'> = {
title,
completed: false,
createTime: Date.now()
};
await TodoDatabase.insert(newTodo);
this.todos = await TodoDatabase.queryAll(); // 直接更新状态(无需额外加载)
catch (error) {
promptAction.showToast({ message: error.message });
}
4.3 单元测试友好性
由于业务逻辑集中在ViewModel,单元测试无需模拟UI环境,可直接验证逻辑正确性:
// TodoViewModel.test.ts
import { TodoViewModel } from ‘./TodoViewModel’;
import { TodoDatabase } from ‘./TodoDatabase’;
// 模拟数据库
class MockTodoDatabase extends TodoDatabase {
static todos: Todo[] = [];
static async insert(todo: Omit<Todo, ‘id’>) {
const id = Date.now().toString();
this.todos.push({ …todo, id });
return id;
static async queryAll() {
return this.todos;
}
// 测试用例
describe(‘TodoViewModel’, () => {
beforeEach(() => {
MockTodoDatabase.todos = []; // 重置数据
TodoDatabase.init = async () => {}; // 替换为模拟初始化
});
test(‘新增待办应同步到列表’, async () => {
const viewModel = new TodoViewModel();
await viewModel.init();
await viewModel.addTodo(‘测试待办’);
expect(viewModel.todos).toEqual([{
id: expect.any(String),
title: '测试待办',
completed: false,
createTime: expect.any(Number)
}]);
});
});
五、总结
MVVM+ArkUI-X的组合,为鸿蒙应用开发带来了“数据-视图”解耦的革命性升级:
职责清晰:Model专注数据,ViewModel封装逻辑,View专注界面;
自动同步:声明式UI与状态管理实现“数据变更→视图更新”的闭环;
跨端友好:业务逻辑可复用,多端仅需调整视图布局;
易于测试:ViewModel可独立测试,降低维护成本。
开发者通过本文的“待办事项”案例,已掌握MVVM+ArkUI-X的核心实践方法。未来可进一步扩展云同步、分布式流转等功能,充分利用鸿蒙“端云协同”的生态优势,构建更复杂、更健壮的原子化服务。
