【高心星出品】
文章目录
- MVVM模式的应用
- ArkUI开发模式图
- 架构设计原则
- 案例
- 运行效果
- 项目结构
- 功能特性
- 开发环境
- model层
- viewmodel层
- view层
MVVM模式的应用
MVVM(Model-View-ViewModel)模式是一种广泛用于应用开发的架构模式,它有助于分离应用程序的业务逻辑、数据和用户界面。在鸿蒙(HarmonyOS)开发中,MVVM模式被广泛应用,特别是在使用ArkUI框架时。
ArkUI采用了 Model-View-ViewModel(MVVM)架构模式。MVVM 将应用分为Model、View和ViewModel三个核心部分,实现数据、视图与逻辑的分离。通过这种模式,UI可以随着状态的变化自动更新,无需手动处理,从而更加高效地管理数据和视图的绑定与更新。
- Model:负责存储和管理应用的数据以及业务逻辑,不直接与用户界面交互。通常从后端接口获取数据,是应用程序的数据基础,确保数据的一致性和完整性。
- View:负责用户界面展示数据并与用户交互,不包含任何业务逻辑。它通过绑定ViewModel层提供的数据来动态更新UI。
- ViewModel:负责管理UI状态和交互逻辑。作为连接Model和View的桥梁,通常一个View对应一个ViewModel,ViewModel监控Model数据的变化,通知View更新UI,同时处理用户交互事件并转换为数据操作。
ArkUI开发模式图
ArkUI的UI开发开发模式即是MVVM模式,而状态变量在MVVM模式中扮演着ViewModel的角色,向上刷新UI,向下更新数据,整体框架如下图:
架构设计原则
不可跨层访问
- View层不可以直接调用Model层的数据,只能通过ViewModel提供的方法进行调用。
- Model层数据,不可以直接操作UI,Model层只能通知ViewModel层数据有更新,由ViewModel层更新对应的数据。
下层不可访问上层数据
下层的数据通过通知模式更新上层数据。在业务逻辑中,下层不可直接写代码去获取上层数据。如ViewModel层的逻辑处理,不能去依赖View层界面上的某个值。
非父子组件间不可直接访问
这是针对View层设计的核心原则,一个组件应该具备这样的逻辑:
- 禁止直接访问父组件(必须使用事件或是订阅能力)。
- 禁止直接访问兄弟组件能力。这是因为组件应该仅能访问自己看的见的子节点(通过传参)和父节点(通过事件或通知),以此完成组件之间的解耦。
对于一个组件,这样设计的原因是:
- 组件自己使用了哪些子组件是明确的,因此可以访问。
- 组件被放置于哪个父节点下是未知的,因此组件想访问父节点,就只能通过通知或者事件能力完成。
- 组件不可能知道自己的兄弟节点是谁,因此组件不可以操纵兄弟节点。
案例
使用MVVM模式开发一个备忘录应用。
运行效果
项目结构
mvvmdemo/
├── src/
│ └── main/
│ ├── ets/
│ │ ├── common/ # 公共组件和常量
│ │ ├── entryability/ # 入口能力
│ │ ├── model/ # 数据模型
│ │ ├── utils/ # 工具类
│ │ ├── view/ # 视图组件
│ │ └── viewmodel/ # 视图模型
│ └── resources/ # 资源文件
功能特性
- 显示待办事项列表
- 添加新的待办事项
- 标记待办事项为已完成/未完成
- 删除待办事项
- 错误处理和加载状态显示
开发环境
- DevEco Studio 5.0或更高版本
- HarmonyOS SDK API 14
- ArkTS
model层
负责描述每一条item数据的结构以及数据的业务逻辑。
TodoItem:
export class TodoItem {
id: number;
title: string;
completed: boolean;
createTime: string;
constructor(id:number,title: string) {
this.id=id
this.title = title;
this.completed = false;
this.createTime = new Date().toLocaleString();
}
toggleComplete(): void {
this.completed = !this.completed;
}
}
TodoService:
import { TodoItem } from './TodoItem';
/**
* 待办事项服务类
* 负责数据的增删改查操作
*/
export class TodoService {
private static instance: TodoService;
private todoList: TodoItem[] = [];
private constructor() {
// 初始化一些示例数据
this.todoList = [
new TodoItem(1,'学习鸿蒙开发'),
new TodoItem(2,'完成MVVM示例项目'),
new TodoItem(3,'阅读鸿蒙文档')
];
}
/**
* 获取单例实例
*/
public static getInstance(): TodoService {
if (!TodoService.instance) {
TodoService.instance = new TodoService();
}
return TodoService.instance;
}
/**
* 获取所有待办事项
*/
public getAllTodos(): TodoItem[] {
return [...this.todoList]
}
/**
* 添加待办事项
*/
public addTodo(title: string) {
const todo = new TodoItem(this.todoList[this.todoList.length-1].id+1,title);
this.todoList.push(todo);
}
/**
* 删除待办事项
*/
public deleteTodo(id: number): boolean {
const index = this.todoList.findIndex(item => item.id === id);
if (index !== -1) {
this.todoList.splice(index, 1);
return true;
}
return false;
}
/**
* 切换待办事项状态
*/
public toggleTodo(id: number): boolean {
const todo = this.todoList.find(item => item.id === id);
if (todo) {
todo.toggleComplete();
return true;
}
return false;
}
}
viewmodel层
TodoViewModel:
import { TodoItem } from '../model/TodoItem';
import { TodoService } from '../model/TodoService';
/**
* 待办事项ViewModel类
* 负责连接Model和View,处理业务逻辑
*/
@Observed
export class TodoViewModel {
private todoService: TodoService;
@Track
todoList: TodoItem[] = [];
@Track
newTodoTitle: string = '';
@Track
isLoading: boolean = false;
@Track
errorMessage: string = '';
constructor() {
this.todoService = TodoService.getInstance();
this.loadTodos();
}
/**
* 加载所有待办事项
*/
public loadTodos(): void {
this.isLoading = true;
this.errorMessage = '';
try {
this.todoList = this.todoService.getAllTodos();
} catch (error) {
this.errorMessage = '加载待办事项失败';
console.error('加载待办事项失败:', error);
} finally {
this.isLoading = false;
}
}
/**
* 添加待办事项
*/
public async addTodo(){
if (!this.newTodoTitle.trim()) {
this.errorMessage = '待办事项不能为空';
return;
}
try {
this.todoService.addTodo(this.newTodoTitle);
this.todoList=this.todoService.getAllTodos()
console.log('gxxt add ',JSON.stringify(this.todoList))
this.newTodoTitle = '';
this.errorMessage = '';
} catch (error) {
this.errorMessage = '添加待办事项失败';
console.error('添加待办事项失败:', error);
}
}
/**
* 删除待办事项
*/
public deleteTodo(id: number): void {
try {
if (this.todoService.deleteTodo(id)) {
this.todoList=this.todoService.getAllTodos()
console.log('gxxt del ',JSON.stringify(this.todoList))
}
} catch (error) {
this.errorMessage = '删除待办事项失败';
console.error('删除待办事项失败:', error);
}
}
/**
* 切换待办事项状态
*/
public toggleTodo(id: number): void {
try {
if (this.todoService.toggleTodo(id)) {
// 更新列表中的项目状态
console.log('gxxt todolist ',JSON.stringify(this.todoList))
this.todoList=this.todoService.getAllTodos()
}
} catch (error) {
this.errorMessage = '更新待办事项状态失败';
console.error('更新待办事项状态失败:', error);
}
}
/**
* 设置新待办事项标题
*/
public setNewTodoTitle(title: string): void {
this.newTodoTitle = title;
}
/**
* 清除错误信息
*/
public clearError(): void {
this.errorMessage = '';
}
}
view层
TodoPage:
import { TodoItem } from '../model/TodoItem';
import { TodoItemView } from '../view/TodoItemView';
import { TodoViewModel } from '../viewmodel/TodoViewModel';
/**
* 待办事项主页面
*/
@Entry
@Component
struct TodoPage {
@State private viewModel: TodoViewModel = new TodoViewModel();
build() {
Column() {
// 标题栏
Row() {
Text('待办事项')
.fontSize(24)
.fontWeight(FontWeight.Bold)
}
.width('100%')
.height(60)
.padding({ left: 20, right: 20 })
.backgroundColor('#F5F5F5')
// 添加待办事项区域
Row() {
TextInput({ placeholder: '输入新的待办事项...' })
.width('80%')
.height(40)
.backgroundColor('#FFFFFF')
.onChange((value: string) => {
this.viewModel.setNewTodoTitle(value);
})
Button('添加')
.width('20%')
.height(40)
.backgroundColor('#007DFF')
.onClick(() => {
this.viewModel.addTodo();
})
}
.width('90%')
.margin({ top: 20, bottom: 10 })
// 错误信息
if (this.viewModel.errorMessage) {
Text(this.viewModel.errorMessage)
.fontSize(14)
.fontColor('#FF0000')
.margin({ bottom: 10 })
}
// 待办事项列表
if (this.viewModel.isLoading) {
LoadingProgress()
.width(50)
.height(50)
.margin({ top: 50 })
} else {
List() {
ForEach(this.viewModel.todoList, (item:TodoItem) => {
ListItem() {
TodoItemView({
todo: item,
onToggle: (id) => this.viewModel.toggleTodo(id),
onDelete: (id) => this.viewModel.deleteTodo(id)
})
}
})
}
.width('90%')
.layoutWeight(1)
}
}
.width('100%')
.height('100%')
.backgroundColor('#FFFFFF')
}
}
TodoItemView:
import { TodoItem } from '../model/TodoItem';
/**
* 待办事项视图组件
*/
@Component
export struct TodoItemView {
@State private todo: TodoItem=new TodoItem(0,'')
private onToggle: ((id: number) => void)=(id:number)=>{};
private onDelete: ((id: number) => void)=(id:number)=>{};
build() {
Row() {
// 复选框
Toggle({ type: ToggleType.Checkbox, isOn: this.todo.completed })
.onChange(() => {
this.onToggle(this.todo.id);
})
.margin({ right: 10 })
// 待办事项标题
Text(this.todo.title)
.fontSize(16)
.fontColor(this.todo.completed ? '#999999' : '#000000')
.decoration({ type:this.todo.completed ? TextDecorationType.LineThrough : TextDecorationType.None})
.layoutWeight(1)
// 删除按钮
Button() {
Image($r('sys.media.ohos_ic_public_device_watch'))
.width(20)
.height(20)
}
.type(ButtonType.Circle)
.backgroundColor(Color.Transparent)
.onClick(() => {
this.onDelete(this.todo.id);
})
}
.width('100%')
.padding(10)
.backgroundColor('#F5F5F5')
.borderRadius(8)
.margin({ bottom: 10 })
}
}