HarmonyOS鸿蒙Next中如何实现MVVM架构?

HarmonyOS鸿蒙Next中如何实现MVVM架构?

  • Model、View、ViewModel如何分层?
  • 如何实现数据双向绑定?
  • 业务逻辑应该放在哪一层?
  • 如何避免View层直接操作数据库?
5 回复

实现方案

1. 架构设计原理

MVVM架构分为三层:

┌─────────────────────────────────────┐
│  View (页面UI)                      │
│  - 只负责UI渲染和用户交互           │
│  - 使用[@State](/user/State)/@Prop绑定数据         │
└─────────────────────────────────────┘
                 ↕ 数据绑定
┌─────────────────────────────────────┐
│  ViewModel (视图模型/Service)       │
│  - 处理业务逻辑                     │
│  - 调用Model层获取数据              │
│  - 将数据转换为View需要的格式       │
└─────────────────────────────────────┘
                 ↕ 数据访问
┌─────────────────────────────────────┐
│  Model (数据层)                     │
│  - Dao: 数据库操作                  │
│  - Entity: 数据模型                 │
└─────────────────────────────────────┘

2. 完整实现代码

第1层: Model - 数据模型

/**
 * 物品实体类
 */
export class Item {
  id: number = 0;
  name: string = '';
  categoryId: number = 0;
  quantity: number = 0;
  unit: string = '';
  expiryDate: number = 0;
  location: string = '';
  
  /**
   * 从数据库记录创建对象
   */
  static fromDb(record: Record<string, Object>): Item {
    const item = new Item();
    item.id = record['id'] as number;
    item.name = record['name'] as string;
    item.categoryId = record['category_id'] as number;
    item.quantity = record['quantity'] as number;
    item.unit = record['unit'] as string;
    item.expiryDate = record['expiry_date'] as number;
    item.location = record['location'] as string;
    return item;
  }
  
  /**
   * 计算剩余天数
   */
  getDaysUntilExpiry(): number {
    const now = Date.now();
    const diff = this.expiryDate - now;
    return Math.ceil(diff / (1000 * 60 * 60 * 24));
  }
  
  /**
   * 是否即将过期(7天内)
   */
  isExpiringSoon(): boolean {
    return this.getDaysUntilExpiry() <= 7 && this.getDaysUntilExpiry() > 0;
  }
  
  /**
   * 是否已过期
   */
  isExpired(): boolean {
    return this.getDaysUntilExpiry() < 0;
  }
}

第1层: Model - 数据访问对象(Dao)

import relationalStore from '@ohos.data.relationalStore';
import { Item } from '../models/Item';
import { DatabaseHelper } from './DatabaseHelper';

const TABLE_NAME = 'item';

export class ItemDao {
  private dbHelper: DatabaseHelper;
  
  constructor() {
    this.dbHelper = DatabaseHelper.getInstance();
  }
  
  /**
   * 插入物品
   */
  async insert(item: Item): Promise<number> {
    const store = this.dbHelper.getStore();
    const valueBucket: relationalStore.ValuesBucket = {
      name: item.name,
      category_id: item.categoryId,
      quantity: item.quantity,
      unit: item.unit,
      expiry_date: item.expiryDate,
      location: item.location,
      create_time: Date.now(),
      update_time: Date.now()
    };
    
    return await store.insert(TABLE_NAME, valueBucket);
  }
  
  /**
   * 查询所有物品
   */
  async findAll(): Promise<Item[]> {
    const store = this.dbHelper.getStore();
    const predicates = new relationalStore.RdbPredicates(TABLE_NAME);
    predicates.orderByDesc('create_time');
    
    const resultSet = await store.query(predicates);
    const items: Item[] = [];
    
    while (resultSet.goToNextRow()) {
      const record = this.resultSetToRecord(resultSet);
      items.push(Item.fromDb(record));
    }
    
    resultSet.close();
    return items;
  }
  
  /**
   * 根据分类查询
   */
  async findByCategoryId(categoryId: number): Promise<Item[]> {
    const store = this.dbHelper.getStore();
    const predicates = new relationalStore.RdbPredicates(TABLE_NAME);
    predicates.equalTo('category_id', categoryId);
    
    const resultSet = await store.query(predicates);
    const items: Item[] = [];
    
    while (resultSet.goToNextRow()) {
      const record = this.resultSetToRecord(resultSet);
      items.push(Item.fromDb(record));
    }
    
    resultSet.close();
    return items;
  }
  
  /**
   * 统计物品总数
   */
  async count(): Promise<number> {
    const store = this.dbHelper.getStore();
    const sql = `SELECT COUNT(*) as total FROM ${TABLE_NAME}`;
    const resultSet = await store.querySql(sql, []);
    
    let count = 0;
    if (resultSet.goToFirstRow()) {
      count = resultSet.getLong(resultSet.getColumnIndex('total'));
    }
    
    resultSet.close();
    return count;
  }
  
  private resultSetToRecord(resultSet: relationalStore.ResultSet): Record<string, Object> {
    const record: Record<string, Object> = {};
    const columnNames = resultSet.columnNames;
    
    for (const name of columnNames) {
      const index = resultSet.getColumnIndex(name);
      const type = resultSet.getColumnType(index);
      
      switch (type) {
        case relationalStore.ColumnType.TYPE_INTEGER:
          record[name] = resultSet.getLong(index);
          break;
        case relationalStore.ColumnType.TYPE_STRING:
          record[name] = resultSet.getString(index);
          break;
        default:
          record[name] = resultSet.getString(index);
      }
    }
    
    return record;
  }
}

第2层: ViewModel - 业务逻辑层(Service)

import { Item } from '../models/Item';
import { ItemDao } from '../database/ItemDao';

/**
 * 物品服务类 - ViewModel层
 * 负责处理物品相关的业务逻辑
 */
export class ItemService {
  private static instance: ItemService;
  private itemDao: ItemDao;
  
  private constructor() {
    this.itemDao = new ItemDao();
  }
  
  static getInstance(): ItemService {
    if (!ItemService.instance) {
      ItemService.instance = new ItemService();
    }
    return ItemService.instance;
  }
  
  /**
   * 获取所有物品
   */
  async getAllItems(): Promise<Item[]> {
    return await this.itemDao.findAll();
  }
  
  /**
   * 添加物品
   */
  async addItem(item: Item): Promise<number> {
    // 业务逻辑: 验证数据
    if (!item.name || item.name.trim() === '') {
      throw new Error('物品名称不能为空');
    }
    
    if (item.quantity < 0) {
      throw new Error('数量不能为负数');
    }
    
    // 调用Dao层插入数据
    return await this.itemDao.insert(item);
  }
  
  /**
   * 获取即将过期的物品
   */
  async getExpiringSoonItems(): Promise<Item[]> {
    const allItems = await this.itemDao.findAll();
    return allItems.filter(item => item.isExpiringSoon());
  }
  
  /**
   * 获取已过期的物品
   */
  async getExpiredItems(): Promise<Item[]> {
    const allItems = await this.itemDao.findAll();
    return allItems.filter(item => item.isExpired());
  }
  
  /**
   * 获取统计信息
   */
  async getStatistics(): Promise<{
    totalCount: number;
    expiringSoonCount: number;
    expiredCount: number;
  }> {
    const allItems = await this.itemDao.findAll();
    
    return {
      totalCount: allItems.length,
      expiringSoonCount: allItems.filter(item => item.isExpiringSoon()).length,
      expiredCount: allItems.filter(item => item.isExpired()).length
    };
  }
  
  /**
   * 根据分类获取物品
   */
  async getItemsByCategory(categoryId: number): Promise<Item[]> {
    return await this.itemDao.findByCategoryId(categoryId);
  }
}

第3层: View - 页面UI

import { Item } from '../models/Item';
import { ItemService } from '../services/ItemService';

@Entry
@Component
struct ItemListPage {
  // ViewModel实例
  private itemService: ItemService = ItemService.getInstance();
  
  // View的状态数据
  [@State](/user/State) items: Item[] = [];
  [@State](/user/State) totalCount: number = 0;
  [@State](/user/State) expiringSoonCount: number = 0;
  [@State](/user/State) isLoading: boolean = false;
  
  /**
   * 页面加载时获取数据
   */
  async aboutToAppear(): Promise<void> {
    await this.loadData();
  }
  
  /**
   * 加载数据 - 通过ViewModel
   */
  private async loadData(): Promise<void> {
    this.isLoading = true;
    
    try {
      // 调用ViewModel获取数据
      this.items = await this.itemService.getAllItems();
      
      // 获取统计信息
      const stats = await this.itemService.getStatistics();
      this.totalCount = stats.totalCount;
      this.expiringSoonCount = stats.expiringSoonCount;
      
    } catch (err) {
      console.error('加载数据失败:', JSON.stringify(err));
    } finally {
      this.isLoading = false;
    }
  }
  
  /**
   * 添加物品
   */
  private async handleAddItem(name: string, quantity: number): Promise<void> {
    try {
      const newItem = new Item();
      newItem.name = name;
      newItem.quantity = quantity;
      newItem.unit = '个';
      newItem.expiryDate = Date.now() + 30 * 24 * 60 * 60 * 1000; // 30天后过期
      
      // 通过ViewModel添加数据
      await this.itemService.addItem(newItem);
      
      // 刷新列表
      await this.loadData();
      
    } catch (err) {
      console.error('添加物品失败:', JSON.stringify(err));
    }
  }
  
  build() {
    Column() {
      // 统计卡片
      Row() {
        this.buildStatCard('总物品', this.totalCount.toString(), '📦');
        this.buildStatCard('即将过期', this.expiringSoonCount.toString(), '⏰');
      }
      .width('100%')
      .padding(16)
      
      // 物品列表
      if (this.isLoading) {
        LoadingProgress()
          .width(50)
          .height(50)
      } else {
        List({ space: 12 }) {
          ForEach(this.items, (item: Item) => {
            ListItem() {
              this.buildItemCard(item);
            }
          })
        }
        .width('100%')
        .layoutWeight(1)
        .padding({ left: 16, right: 16 })
      }
    }
    .width('100%')
    .height('100%')
    .backgroundColor('#F5F5F5')
  }
  
  /**
   * 统计卡片组件
   */
  @Builder
  buildStatCard(title: string, value: string, icon: string) {
    Column() {
      Text(icon).fontSize(32);
      Text(value)
        .fontSize(24)
        .fontWeight(FontWeight.Bold)
        .margin({ top: 8 });
      Text(title)
        .fontSize(14)
        .fontColor('#666666')
        .margin({ top: 4 });
    }
    .width('45%')
    .padding(16)
    .backgroundColor(Color.White)
    .borderRadius(12)
  }
  
  /**
   * 物品卡片组件
   */
  @Builder
  buildItemCard(item: Item) {
    Row() {
      Column({ space: 4 }) {
        Text(item.name)
          .fontSize(16)
          .fontWeight(FontWeight.Medium);
        
        Text(`数量: ${item.quantity} ${item.unit}`)
          .fontSize(14)
          .fontColor('#666666');
        
        // 显示过期状态
        if (item.isExpired()) {
          Text('已过期')
            .fontSize(12)
            .fontColor('#F44336')
            .padding({ left: 8, right: 8, top: 2, bottom: 2 })
            .backgroundColor('#FFEBEE')
            .borderRadius(4);
        } else if (item.isExpiringSoon()) {
          Text(`${item.getDaysUntilExpiry()}天后过期`)
            .fontSize(12)
            .fontColor('#FF9800')
            .padding({ left: 8, right: 8, top: 2, bottom: 2 })
            .backgroundColor('#FFF3E0')
            .borderRadius(4);
        }
      }
      .alignItems(HorizontalAlign.Start)
      .layoutWeight(1)
    }
    .width('100%')
    .padding(16)
    .backgroundColor(Color.White)
    .borderRadius(12)
  }
}

3. 运行效果

界面显示:

┌──────────────────────────────┐
│  📦           ⏰              │
│  25           3              │
│  总物品       即将过期        │
└──────────────────────────────┘

┌──────────────────────────────┐
│  苹果                        │
│  数量: 10 个                 │
│  [ 5天后过期 ]               │
└──────────────────────────────┘

┌──────────────────────────────┐
│  牛奶                        │
│  数量: 2 瓶                  │
│  [ 已过期 ]                  │
└──────────────────────────────┘

关键要点

1. 职责分离

Model层:

  • ✅ 只负责数据定义和数据访问
  • ✅ 不包含业务逻辑
  • ✅ 不直接与View交互

ViewModel层(Service):

  • ✅ 处理所有业务逻辑
  • ✅ 调用Model获取数据
  • ✅ 数据格式转换和验证
  • ✅ 单例模式管理

View层:

  • ✅ 只负责UI渲染
  • ✅ 通过ViewModel获取数据
  • ✅ 使用@State实现响应式更新
  • ✅ 不直接操作数据库

2. 数据流向

用户操作 → View → ViewModel → Model → 数据库
                         ↓
                     业务逻辑处理
                         ↓
数据库 → Model → ViewModel → View → UI更新

3. 单例模式使用

Service层使用单例模式的好处:

  • 全局共享实例,节省内存
  • 避免重复创建Dao对象
  • 便于数据缓存管理
export class ItemService {
  private static instance: ItemService;
  
  private constructor() {
    // 私有构造函数
  }
  
  static getInstance(): ItemService {
    if (!ItemService.instance) {
      ItemService.instance = new ItemService();
    }
    return ItemService.instance;
  }
}

4. 状态管理

View层使用ArkUI状态管理:

  • [@State](/user/State): 组件内部状态
  • @Prop: 父组件传入的属性
  • @Link: 双向绑定
  • @Provide/@Consume: 跨组件传递
[@State](/user/State) items: Item[] = [];  // 状态变化会自动触发UI更新

最佳实践

1. 错误处理

在ViewModel层统一处理错误:

async addItem(item: Item): Promise<number> {
  try {
    // 验证
    if (!item.name) {
      throw new Error('物品名称不能为空');
    }
    
    // 调用Dao
    return await this.itemDao.insert(item);
    
  } catch (err) {
    console.error('添加物品失败:', JSON.stringify(err));
    throw err;  // 向上抛出,由View层处理
  }
}

2. 数据转换

在ViewModel层处理数据转换:

async getExpiringSoonItems(): Promise<Item[]> {
  const allItems = await this.itemDao.findAll();
  
  // 业务逻辑: 筛选即将过期的物品
  return allItems.filter(item => {
    const days = item.getDaysUntilExpiry();
    return days <= 7 && days > 0;
  });
}

3. 缓存策略

在Service层添加缓存:

export class ItemService {
  private cachedItems: Item[] = [];
  private cacheTime: number = 0;
  private CACHE_DURATION = 5 * 60 * 1000; // 5分钟
  
  async getAllItems(): Promise<Item[]> {
    const now = Date.now();
    
    // 缓存有效,直接返回
    if (this.cachedItems.length > 0 && 
        (now - this.cacheTime) < this.CACHE_DURATION) {
      return this.cachedItems;
    }
    
    // 缓存失效,重新获取
    this.cachedItems = await this.itemDao.findAll();
    this.cacheTime = now;
    return this.cachedItems;
  }
  
  // 清除缓存
  clearCache(): void {
    this.cachedItems = [];
    this.cacheTime = 0;
  }
}

常见问题

Q1: View层可以直接调用Dao吗?

不推荐:

// 不要这样做
[@State](/user/State) items: Item[] = [];
private itemDao = new ItemDao();

async aboutToAppear() {
  this.items = await this.itemDao.findAll();  // ❌ 跳过ViewModel
}

推荐:

// 通过Service层
[@State](/user/State) items: Item[] = [];
private itemService = ItemService.getInstance();

async aboutToAppear() {
  this.items = await this.itemService.getAllItems();  // ✅ 经过ViewModel
}

Q2: 业务逻辑应该放在哪里?

全部放在ViewModel(Service)层:

  • 数据验证
  • 数据转换
  • 复杂计算
  • 状态判断

Q3: Model层可以有业务方法吗?

可以有简单的辅助方法:

export class Item {
  // ✅ 简单的计算方法
  getDaysUntilExpiry(): number {
    return Math.ceil((this.expiryDate - Date.now()) / (1000 * 60 * 60 * 24));
  }
  
  // ✅ 简单的状态判断

更多关于HarmonyOS鸿蒙Next中如何实现MVVM架构?的实战系列教程也可以访问 https://www.itying.com/category-93-b0.html


MVVM模式介绍

概念

在应用开发中,UI更新需要实时同步数据状态变化,这直接影响应用程序的性能和用户体验。为了解决数据与UI同步的复杂性,ArkUI采用了 Model-View-ViewModel(MVVM)架构模式。MVVM 将应用分为Model、View和ViewModel三个核心部分,实现数据、视图与逻辑的分离。通过这种模式,UI可以自动更新状态变化,从而更高效地管理数据和视图的绑定与更新。

  • View:用户界面层。负责用户界面展示并与用户交互,不包含任何业务逻辑。它通过绑定ViewModel层提供的数据实现动态更新。

  • Model:数据访问层。以数据为中心,不直接与用户界面交互。负责数据结构定义,数据管理(获取、存储、更新等),以及业务逻辑处理。

  • ViewModel:表示逻辑层。作为连接Model和View的桥梁,通常一个View对应一个ViewModel。View和ViewModel有两种通信方式:

    1.方法调用:View通过事件监听用户行为,在回调里面触发ViewModel层的方法。例如当View监听到用户Button点击行为,调用ViewModel对应的方法,处理用户操作。 2.双向绑定:View绑定ViewModel的数据,实现双向同步。

ArkUI的UI开发模式就属于MVVM模式,通过对MVVM概念的基本介绍,开发者大致能猜到状态管理能在MVVM中起什么样的作用,状态管理旨在数据驱动更新,让开发者只用关注页面设计,而不去关注整个UI的刷新逻辑,数据的维护也无需开发者进行感知,由状态变量自动更新完成,而这就是属于ViewModel层所需要支持的内容,因此开发者使用MVVM模式开发自己的应用是最省心省力的。

在HarmonyOS Next中实现MVVM架构,主要使用ArkUI框架的声明式UI开发范式。通过@State@Prop@Link等装饰器管理状态数据,实现数据与UI自动绑定。ViewModel层处理业务逻辑,Model层管理数据源,View层由ArkUI组件构成。数据变化通过装饰器驱动UI自动更新,实现双向数据绑定。

在HarmonyOS Next中实现MVVM架构,主要依赖于ArkTS的声明式UI和响应式数据管理能力。以下是针对您问题的具体实现方案:

1. 分层架构

  • Model层:负责数据模型和业务逻辑。通常包含实体类(Entity)、数据访问对象(DAO)或Repository,以及网络请求、数据库操作等纯数据逻辑。
  • View层:由ArkUI的声明式UI组件构成(如@Component装饰的组件)。它仅负责数据展示和用户交互事件的传递,不包含任何业务逻辑。
  • ViewModel层:作为View和Model的桥梁。它持有与UI相关的状态数据(通常使用@State, @Prop, @Link, @Provide, @Consume等装饰器),并暴露方法供View调用。ViewModel通过调用Model层的方法来获取或更新数据,并将数据变化响应式地同步到View。

2. 数据双向绑定 HarmonyOS Next通过ArkTS的装饰器实现数据的响应式更新,这是单向数据流的体现。对于类似“双向绑定”的需求(如输入框):

  • 使用@State装饰器在ViewModel或组件内定义数据。
  • 在UI中使用该状态变量,例如:TextInput({ text: this.inputText })。当用户在UI中修改文本时,通过事件(如onChange)回调来更新this.inputText,从而驱动UI重新渲染。这遵循“状态驱动UI”的原则。

3. 业务逻辑存放位置

  • 核心业务逻辑、数据计算、数据验证等应置于Model层(如Service、Repository或独立的工具类中)。
  • 与UI状态紧密相关的逻辑(如格式化显示数据、处理UI交互事件)可放在ViewModel层。ViewModel应尽量轻薄,仅做简单的协调和转换,复杂的计算应委托给Model层。

4. 避免View层直接操作数据库

  • 严格分层:在架构上禁止View层(UI组件)持有或引入任何数据库操作(如RDB、对象数据库)的API。
  • 依赖方向:View依赖ViewModel,ViewModel依赖Model。View通过调用ViewModel暴露的方法或绑定其状态来间接获取数据。所有数据库操作(增删改查)的代码必须封装在Model层的Repository或DAO类中。
  • 数据流:当需要数据时,View触发ViewModel中的方法,ViewModel调用Model层的Repository获取数据(可能来自数据库、网络或内存),然后将结果转换为UI状态,View自动响应更新。

示例代码结构

// Model层
@Entity
class User {
  // 字段定义
}
class UserRepository {
  async getUser(id: number): Promise<User> { /* 数据库查询 */ }
}

// ViewModel层
class UserViewModel {
  private userRepo: UserRepository = new UserRepository();
  @State user: User = new User();

  async loadUser(id: number) {
    this.user = await this.userRepo.getUser(id); // 调用Model层
  }
}

// View层
@Component
struct UserPage {
  private vm: UserViewModel = new UserViewModel();

  build() {
    Column() {
      Text(this.vm.user.name) // 绑定ViewModel状态
      Button('加载')
        .onClick(() => {
          this.vm.loadUser(1); // 触发ViewModel逻辑
        })
    }
  }
}

通过以上方式,可以在HarmonyOS Next中构建一个清晰、可测试且易于维护的MVVM架构,确保关注点分离和数据流的单向性。

回到顶部