乐于分享
好东西不私藏

一个例子说明鸿蒙应用的MVVM架构

一个例子说明鸿蒙应用的MVVM架构

今天看到有些同学的代码,一个页面就有上千行甚至更多,有图有真相,
用李白的诗就说这代码:飞流直下三千尺,疑是银河落九天。
所有的代码堆在一个页面文件里面,包括但不限于:页面布局,业务逻辑,模型接口,数据处理,网络请求等,混在一起,所谓一万恒河水,十万八千虫,真的是包罗万象。这时候想定位个问题或者做个改动,就特别的忐忑,担心会一不小心打开了潘多拉盒子,或者触发了多米诺骨牌的开关。
说来惭愧,我的音频播放器目前来说,也是一个文件包打天下,虽然说了要改,但也一直没改,立个FLAG 5月底之前改掉。那么有同学就要问了,你说要改,但该怎么改呢?
本文通过一个简单的人员列表的例子,学习下鸿蒙官方推荐的“代码收纳术”—— MVVM 架构。
1、MVVM是什么?
记得多年以前的IT圈有个说法叫,如果觉得业务逻辑不够清晰,那么就加一层,如果还觉得不够清晰,那就再加一层。鸿蒙的MVVM也是再加一层以后的结果

MVVM 它是 Model-View-ViewModel 的缩写,三层分工如下表

字母
中文名
干啥的
像啥
M
数据模型Model
存数据、定义结构
仓库,用来保存货架和货物
V
视图层View
展示界面、响应用户
前台业务员,只管接待收发货物
VM
视图模型ViewModel
管状态、写逻辑
店长,协调仓库和前台

2、一个人员列表的例子

目录结构如下图所示:
2.1 Model:
定义数据模型,确定”人“都有哪些属性,比如人必须有证件号,姓名,年龄,性别,管理员说了,只有能提供这几个信息的人,才能入住我们的仓库。
export interface PersonInfo {  idnumber;  namestring;  genderstring;  agenumber;}
2.2. ViewModel 层:状态管家 + 业务大脑
@ObservedV2export classPersonViewModel{  @Trace persons: PersonInfo[] = [];  // 数据变了,界面自动刷新  shouldHighlightId(person): boolean {    return person.age < 14;  // 未成年ID标红  }
店长要操心的事情:一旦有人入住或者退房,要通知前台接待员(管理数据,一旦变化主动通知前台界面刷新展示)

2.3. View 层:只画界面,不处理业务逻辑

@Entry@ComponentV2struct Index {  @Local viewModelPersonViewModel = new PersonViewModel();  build() {    List() {      ForEach(this.viewModel.persons(person) => {        Text(person.name)          .fontColor(this.viewModel.shouldHighlightId(person) ? '#FF0000' : '#333333')      })    }  }}
前台小姐姐啥也不问啥也不说,店长给啥就展示啥。

三、依赖关系

如图所示:

只有单项依赖,VIEW依赖于VIEWMODEL,VIEWMODEL依赖于MODEL,这样上层的改动不会影响到下层的功能变化,而下层的改动,也不用动界面,下层甚至对上层的存在无感知。
四、看看例子代码:
我们的完成一个人员信息的列表显示,其中把年龄小于14岁的标记未成年标签,并且把年龄小于十四岁,且名字叫”赵六“的ID标红显示。
4.1 model文件:
// 定义 PersonInfo 接口
export interface PersonInfo {
idnumber;
namestring;
genderstring;
agenumber;
}
// 提供一些示例数据
export const samplePersonsPersonInfo[] [
id1name'张三'gender''age10 },
id2name'李四'gender''age15 },
id3name'王五'gender''age},
id4name'赵六'gender''age20 },
id5name'赵六'gender''age10 }
];
4.2 utils文件
import PersonInfo from '../model/PersonModel';
export class PersonUtils {
// 判断是否为未成年人(年龄 < 14
static isChildren(agenumber)boolean {
return age 14;
}
// 判断某条记录中的 ID 是否需要红色显示
// 当年龄小于14岁时,返回 true 表示需要红色显示 ID
static shouldHighlightId(personPersonInfo)boolean {
return PersonUtils.isChildren(person.age&& person.name ==='赵六';
}
}
4.3 viewmodel文件
import PersonInfosamplePersons from '../model/PersonModel';
import PersonUtils from '../utils/PersonUtils';
@ObservedV2
export class PersonViewModel {
@Trace personsPersonInfo[] [];  //引用model
@Trace titlestring '人员信息列表';
constructor() {
this.loadPersons();
}
// 加载人员数据
loadPersons()void {
this.persons [...samplePersons];
}
// 获取人员总数
getTotalCount()number {
return this.persons.length;
}
// 获取成年人数量
getAdultCount()number {
return this.persons.filter(person => !PersonUtils.isChildren(person.age)).length;
}
// 获取未成年人数量
getChildrenCount()number {
return this.persons.filter(person => PersonUtils.isChildren(person.age)).length;
}
// 判断是否需要高亮显示 ID
shouldHighlightId(personPersonInfo)boolean {
return PersonUtils.shouldHighlightId(person);
}
// 获取年龄显示颜色(未成年人显示蓝色)
getAgeColor(personPersonInfo)string {
return PersonUtils.isChildren(person.age'#0066CC'#333333';
}
}
4.4 view文件
import PersonInfo from '../model/PersonModel';
import PersonViewModel from '../viewmodel/PersonViewModel';
import PersonUtils from '../utils/PersonUtils';
@Entry
@ComponentV2
struct Index {
@Local viewModelPersonViewModel new PersonViewModel();    //引用ViewModel
build() {
Column() {
// 标题
Text(this.viewModel.title)
.fontSize(24)
.fontWeight(FontWeight.Bold)
.margin(top20bottom10 })
// 人员列表标题
Text('人员列表')
.fontSize(18)
.fontWeight(FontWeight.Medium)
.alignSelf(ItemAlign.Start)
.margin(left10bottom10 })
// 人员列表
List() {
ForEach(this.viewModel.persons(personPersonInfo//引用model
indexnumber=> {
ListItem() {
this.PersonListItem(person)
}
                }(personPersonInfo=> person.id.toString())
}
.width('100%')
.layoutWeight(1)
.scrollBar(BarState.Auto)
}
.width('100%')
.height('100%')
.padding(16)
.backgroundColor('#FFFFFF')
}
// 人员列表项组件
@Builder
PersonListItem(personPersonInfo{
Row() {
// ID 显示 - 未成年人用红色
Text(`#${person.id}`)
.fontSize(16)
.fontColor(this.viewModel.shouldHighlightId(person'#FF0000'#333333')
.fontWeight(this.viewModel.shouldHighlightId(personFontWeight.Bold FontWeight.Normal)
.width(60)
// 姓名
Text(person.name)
.fontSize(16)
.fontColor('#333333')
.width(80)
// 性别
Text(person.gender)
.fontSize(14)
.fontColor('#666666')
.width(50)
// 年龄
Text(`${person.age}`)
.fontSize(14)
.fontColor(this.viewModel.getAgeColor(person))
.width(60)
// 未成年人标签
if (PersonUtils.isChildren(person.age)) {
Text('未成年')
.fontSize(11)
.fontColor('#FFFFFF')
.backgroundColor('#FF6600')
.borderRadius(12)
.padding(left8right8top2bottom})
.margin(left10 })
}
        }
.width('100%')
.height(56)
.padding(left12right12 })
.borderRadius(8)
// .backgroundColor(index % 2 === 0 ? '#FFFFFF' : '#F9F9F9')
.margin(bottom})
}
}
后记:

刚开始可能觉得“多写几个文件好麻烦”,但等项目上了规模,改需求的时候就会发现——

当初多分的层,都是为未来的自己少掉头发,

什么?  头发,什么是头发?