HarmonyOS鸿蒙Next中如何实现MVVM架构?
HarmonyOS鸿蒙Next中如何实现MVVM架构?
- Model、View、ViewModel如何分层?
- 如何实现数据双向绑定?
- 业务逻辑应该放在哪一层?
- 如何避免View层直接操作数据库?
实现方案
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模式开发自己的应用是最省心省力的。
666
在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架构,确保关注点分离和数据流的单向性。

