MVVM+ArkUI-X:鸿蒙应用中“数据-视图”解耦的设计模式升级

进修的泡芙
发布于 2025-6-11 22:21
浏览
0收藏

引言

在传统鸿蒙应用开发中,我们常面临“数据-视图”强耦合的困境: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的核心实践方法。未来可进一步扩展云同步、分布式流转等功能,充分利用鸿蒙“端云协同”的生态优势,构建更复杂、更健壮的原子化服务。

分类
收藏
回复
举报
回复
    相关推荐