# harmonyOS_mvvmV2Case **Repository Path**: BlocksArchitect/harmony-os_mvvm-v2-case ## Basic Information - **Project Name**: harmonyOS_mvvmV2Case - **Description**: 鸿蒙开发V2状态管理MVVM最小化例子 - **Primary Language**: Unknown - **License**: Apache-2.0 - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2025-11-17 - **Last Updated**: 2025-11-17 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README ### 🎯 : MVVM V2状态管理 开发模式,看完即懂! ⭐⭐⭐⭐⭐⭐ 📢 **前言** 📖 **项目中用到的V2的装饰器相关的讲解楼主都有博文输出** - MVVM V1 开发模式: [相关MVVM模式讲解在这一篇V1的文章输出过](https://developer.huawei.com/consumer/cn/blog/topic/03198082343042257) - @Local: [浅入@Local装饰器: 组件内部状态](https://developer.huawei.com/consumer/cn/blog/topic/03197637455111231) - @Param: [浅入@Param: 组件外部输入](https://developer.huawei.com/consumer/cn/blog/topic/03197766964407255) - @Event: [浅入@Event装饰器: 规范组件输出](https://developer.huawei.com/consumer/cn/blog/topic/03197937825125309) - @Monitor: [浅入@Monitor装饰器: 状态变量修改监听](https://developer.huawei.com/consumer/cn/blog/topic/03198073553936313) - PersistenceV2: [浅入PersistenceV2: 持久化储存UI状态](https://developer.huawei.com/consumer/cn/blog/topic/03198369898358293) - Repeat: [浅入Repeat: 子组件复用](https://developer.huawei.com/consumer/cn/blog/topic/03198444499085015) ### 🔥🔥🔥 **实战环节-基础列表(点赞、删除、添加)开发** 🧱 **MVVM模式组织结构** ```javascript ├── src │ ├── ets │ │ ├── components │ │ │ ├──model │ │ │ ├── ListItemModel.ets │ │ │ └── ListModel.ets │ │ │ ├──views │ │ │ ├── ListComponent.ets │ │ │ └── ListItemComponent.ets │ │ │ ├──viewModel │ │ │ ├── ListItemViewModel.ets │ │ │ └── ListViewModel.ets │ │ ├── pages │ │ │ ├── Index.ets │ └── resources │ │ ├── rawfile │ │ │ ├── listsData.json │ ``` 🧱 **model** ```javascript // model/ListItemModel.ets /** * 接口每项的字段 */ export class ListItemModel { label: string = '' collect: boolean = false } // model/ListModel.ets /** * 提供原始数据和接口API设定 */ import { common } from '@kit.AbilityKit' import { util } from '@kit.ArkTS' import { ListItemModel } from './ListItemModel' export class ListModel { lists: Array = [] async getMockData(context: common.UIAbilityContext) { const getJson = await context.resourceManager.getRawFileContent('listsData.json') const textDecoderOptions: util.TextDecoderOptions = { ignoreBOM: true } const textDecoder = util.TextDecoder.create('utf-8', textDecoderOptions) const result = textDecoder.decodeToString(getJson, { stream: false }) this.lists = JSON.parse(result) } } ``` 🧱 **views** ```javascript // views/ListItemComponent.ets /** * 列表项(item) */ import ListItemViewModel from '../viewModel/ListItemViewModel'; @ComponentV2 export struct ListItemComponent { @Require @Param itemData: ListItemViewModel = new ListItemViewModel() @Event removeOneself: () => void = () => {} build() { Row() { Text(this.itemData.label) Blank() Row({ space: 10 }) { Text('删除') .fontSize(20) .fontColor(Color.White) .backgroundColor(Color.Red) .padding({ left: 3, right: 3, top: 3, bottom: 3 }) .borderRadius(5) .onClick(() => this.removeOneself()) Text() .width(50) .aspectRatio(1) .borderRadius('50%') .backgroundColor(this.itemData.collect ? '#ff4cd1d6' : Color.Transparent) .borderWidth(1) .borderColor(Color.Black) .onClick(() => this.itemData.updateIsCollect(this.getUIContext())) } } .width('100%') .height(80) .padding({ left: 40, right: 20 }) .borderWidth(1) .borderColor(Color.Black) .borderRadius(12) } } // views/ListComponent.ets /** * 列表循环 */ import { common } from "@kit.AbilityKit" import ListViewModel from "../viewModel/ListViewModel" import ListItemViewModel from "../viewModel/ListItemViewModel" import { ListItemComponent } from "./ListItemComponent" @ComponentV2 export struct ListComponent { private context = this.getUIContext().getHostContext() as common.UIAbilityContext @Local listViewModel: ListViewModel = new ListViewModel() async aboutToAppear() { await this.listViewModel.getMockData(this.context) } build() { Column({ space: 10 }) { Text('点击:添加固定数据 (后羿)') .fontSize(20) .fontColor(Color.White) .backgroundColor(Color.Green) .padding({ left: 3, right: 3, top: 3, bottom: 3 }) .borderRadius(5) .onClick(() => this.listViewModel.addItemData()) Column({ space: 4 }) { Repeat(this.listViewModel.lists) .each((obj: RepeatItem) => { ListItemComponent({ itemData: obj.item, removeOneself: () => { this.listViewModel.removeItemData(obj.item) } }) }) } .width('100%') } .width('100%') .height('100%') .alignItems(HorizontalAlign.Start) } } ``` 🧱 **viewModel** ```javascript // viewModel/ListItemViewModel.ets /** * 列表(item)自身的事件定义 */ import { ListItemModel } from "../model/ListItemModel" @ObservedV2 export default class ListItemViewModel { @Trace label: string = '' @Trace collect: boolean = false setItemData(item: ListItemModel) { this.label = item.label this.collect = item.collect } updateIsCollect(uiContext: UIContext): void { this.collect = !this.collect uiContext.getPromptAction().showToast({ message: this.collect ? '收藏成功' : '取消收藏' }) } } // viewModel/ListViewModel.ets /** * 中介层提供处理的好数据给视图展示 */ import ListItemViewModel from "./ListItemViewModel" import { common } from "@kit.AbilityKit" import { ListModel } from "../model/ListModel" import { PersistenceV2, promptAction, Type, UIUtils } from "@kit.ArkUI" // 持久化失败时调用 PersistenceV2.notifyOnError((key: string, reason: string, msg: string) => { console.error(`error key: ${key}, reason: ${reason}, message: ${msg}`); }) @ObservedV2 export default class ListViewModel { @Type(ListItemViewModel) @Trace lists: Array = [] // TODO: 有个弊端类被实例化的时候会触发一次,想要解决可以使用 API 20+ addMonitor 这个方法动态添加 @Monitor('lists.length') listsLengthChange(monitor: IMonitor) { monitor.dirty.forEach((path: string) => { if (monitor.value(path)?.before !== monitor.value(path)?.now) { promptAction.showToast({ message: `当前英雄数量${this.lists.length}`}) } }) } // TODO: 这里是配合下方addMonitor的使用方式 // listsLengthChange(monitor: IMonitor) { // monitor.dirty.forEach((path: string) => { // if (monitor.value(path)?.before !== monitor.value(path)?.now) { // promptAction.showToast({ message: `当前英雄数量${this.lists.length}`}) // } // }) // } async getMockData(context: common.UIAbilityContext) { this.lists = PersistenceV2.connect(ListViewModel, 'ListViewModel', () => new ListViewModel())!.lists if (this.lists.length <= 0) { const result = new ListModel() await result.getMockData(context) for (let item of result.lists) { let listItemViewModel = new ListItemViewModel() listItemViewModel.setItemData(item) this.lists.push(listItemViewModel) } PersistenceV2.connect(ListViewModel, 'ListViewModel', () => new ListViewModel())! } // TODO:配合上方的使用方式 // UIUtils.addMonitor(this, 'lists.length', this.listsLengthChange) } addItemData() { let listItemViewModel = new ListItemViewModel() listItemViewModel.label = '后羿' listItemViewModel.collect = false this.lists.unshift(listItemViewModel) } removeItemData(removedItem: ListItemViewModel) { this.lists.splice(this.lists.indexOf(removedItem), 1) } } ``` 🧱 **listsData** ```javascript // src/main/resources/rawfile/listsData.json /** * mock字段 * label 标题 * collect 收藏 */ [ {"label": "后羿", "collect": false}, {"label": "孙尚香", "collect": false}, {"label": "黄忠", "collect": false}, {"label": "马可波罗", "collect": false}, {"label": "公孙离", "collect": false}, {"label": "狄仁杰", "collect": false}, {"label": "敖丙", "collect": false} ] ``` ### 🎉 成果图 ![输入图片说明](https://foruda.gitee.com/images/1763393273623090541/ceaa7dcc_11991167.gif "mvvm2.gif") 🌍️ **前往gitee仓库** [链接](https://gitee.com/BlocksArchitect/harmony-os_mvvm-v2-case) 📝 V1过渡到V2爽点 - V1例子ListViewModel,里面的lists字段赋值必须要使用@Observed装饰的类型,不然在视图层显示不出数据 - V2使用@ObservedV2 + @Trace,被@Trace装饰的属性具有被观测变化的能力 - V2新增或者优化的装饰器让逻辑以及结构上更加清晰明了,易追溯 ```javascript V1 @Observed export class ListViewModelArray extends Array { } @Observed export default class ListViewModel { @Track lists: ListViewModelArray = new ListViewModelArray() } V2 @ObservedV2 export default class ListViewModel { @Trace lists: Array = [] } ``` 🌸🌼🌺