购物车MVVM案例(V2版本)
这个案例需要了解一些状态管理的知识,比如 @Param、@Require、@ObservedV2、@ComponentV2、@Trace、@Event下面我们就先来了解本案例用到的,如下:
@Param 组件外部输入
-
@Param表示组件从外部传入的状态,使得父子组件之间的数据能够进行同步:
-
@Param装饰的变量支持本地初始化,但是不允许在组件内部直接修改变量本身。
-
被@Param装饰的变量能够在初始化自定义组件时从外部传入,当数据源也是状态变量时,数据源的修改会同步给@Param。
-
@Param可以接受任意类型的数据源,包括普通变量、状态变量、常量、函数返回值等。
-
@Param装饰的变量变化时,会刷新该变量关联的组件。
-
@Param支持观测number、boolean、string、Object、class等基本类型以及Array、Set、Map、Date等内嵌类型。
-
对于复杂类型如类对象,@Param会接受数据源的引用。在组件内可以修改类对象中的属性,该修改会同步到数据源。
-
@Param的观测能力仅限于被装饰的变量本身。当装饰简单类型时,对变量的整体改变能够观测到;当装饰对象类型时,仅能观测对象整体的改变;当装饰数组类型时,能观测到数组整体以及数组元素项的改变;当装饰Array、Set、Map、Date等内嵌类型时,可以观测到通过API调用带来的变化。详见观察变化。
-
@Param支持null、undefined以及联合类型。
@Require
- 当@Require装饰器和@Prop、@State、@Provide、@BuilderParam、普通变量(无状态装饰器修饰的变量)结合使用时,在构造该自定义组件时,@Prop、@State、@Provide、@BuilderParam和普通变量(无状态装饰器修饰的变量)必须在构造时传参。
- 限制条件: @Require装饰器仅用于装饰struct内的@Prop、@State、@Provide、@BuilderParam和普通变量(无状态装饰器修饰的变量)。
@ObservedV2装饰器和@Trace装饰器:类属性变化观测
-
@ObservedV2装饰器与@Trace装饰器用于装饰类以及类中的属性,使得被装饰的类和属性具有深度观测的能力:
-
@ObservedV2装饰器与@Trace装饰器需要配合使用,单独使用@ObservedV2装饰器或@Trace装饰器没有任何作用。
-
被@Trace装饰器装饰的属性property变化时,仅会通知property关联的组件进行刷新。
-
在嵌套类中,嵌套类中的属性property被@Trace装饰且嵌套类被@ObservedV2装饰时,才具有触发UI刷新的能力。
-
在继承类中,父类或子类中的属性property被@Trace装饰且该property所在类被- @ObservedV2装饰时,才具有触发UI刷新的能力。
-
未被@Trace装饰的属性用在UI中无法感知到变化,也无法触发UI刷新。
-
@ObservedV2的类实例目前不支持使用JSON.stringify进行序列化。
@Event装饰器:组件输出
由于@Param装饰的变量在本地无法更改,使用@Event装饰器装饰回调方法并调用,可以实现更改数据源的变量,再通过@Local的同步机制,将修改同步回@Param,以此达到主动更新@Param装饰变量的效果。
@Event用于装饰组件对外输出的方法:
-
@Event装饰的回调方法中参数以及返回值由开发者决定。
-
@Event装饰非回调类型的变量不会生效。当@Event没有初始化时,会自动生成一个空的函数作为默认回调。
-
当@Event未被外部初始化,但本地有默认值时,会使用本地默认的函数进行处理。
@Param标志着组件的输入,表明该变量受父组件影响,而@Event标志着组件的输出,可以通过该方法影响父组件。使用@Event装饰回调方法是一种规范,表明该回调作为自定义组件的输出。父组件需要判断是否提供对应方法用于子组件更改@Param变量的数据源。
购物车页面,使用 ComponentV2 装饰,并绑定一个pageVM
@Entry
@ComponentV2
struct ShoppingCartV2 {
/**
* page绑定的vm
*/
vm: ShoppingCartVMV2 = new ShoppingCartVMV2()
onPageShow(): void {
this.vm.loadData()
}
//
build() {
Flex({ direction: FlexDirection.Column }) {
List() {
ForEach(this.vm.cellVMArr, (item: ShoppingCartCellVMV2) => {
ListItem() {
ShoppingCartCellV2({ cellVM: item, pageVM: this.vm })
}
})
}
Row() {
// 单选
UgCheckBoxV2({
selectedItem: this.vm, title: "全选", didClick: (e) => {
console.log("e")
this.vm.didSelectAll(e)
}
})
.padding({ right: 30 })
// 选择个数
Text() {
Span('已选 ')
.fontSize(15)
Span(this.vm.selectedCount.toString())
.fontSize(18)
.fontColor(Color.Red)
.fontWeight(FontWeight.Medium)
}
.fontSize(15)
.width(80)
// 总价
Text() {
Span('总价 ')
.fontSize(15)
Span('¥')
.fontSize(13)
.fontColor(Color.Red)
Span(this.vm.selectedAllPrice)
.fontSize(18)
.fontColor(Color.Red)
.fontWeight(FontWeight.Medium)
}
}
.padding({ left: 20, right: 20 })
.height(80)
.width('100%')
.backgroundColor(Color.White)
}
}
}
pageVM
/**
* 购物车页面的VM
*/
@ObservedV2
class ShoppingCartVMV2 {
/**
* 选择的商品个数
*/
@Trace selectedCount: number = 0
/**
* 总价
*/
@Trace selectedAllPrice: string = '0.0'
/**
* 是否全选
*/
@Trace selected: boolean = false
/**
* moc 请求获取的商品模型数组
*/
@Trace cellVMArr: ShoppingCartCellVMV2[] = []
/**
* 获取随机数
*/
generateRandomInRange(min: number, max: number): number {
return Math.floor(Math.random() * (max - min + 1)) + min;
}
/**
* 底部全选按钮点击事件
*/
didSelectAll(isSelectAll: boolean) {
for (let cellVM of this.cellVMArr) {
cellVM.selected = isSelectAll
}
this.checkSelected()
}
/**
* 选择的个数有变动&触发总价变动
*/
checkSelected() {
let count: number = 0
let price: number = 0.0
this.cellVMArr.filter((e) => e.selected).forEach((e) => {
count += e.buyCount;
price += e.buyCount * Number(e.getPriceNumber())
});
this.selectedCount = count
this.selectedAllPrice = price.toFixed(2)
this.selected = this.cellVMArr.every((e) => e.selected)
}
/**
* moc数据请求
*/
loadData() {
// name
const productNames: string[] = [
'我是一个小保安',
'保卫一方平安',
'喜欢吃小熊饼干',
'喜欢业主小丹',
'起个名字好难',
'我就叫好商品',
];
// icon
let productIcons: string[] = [];
for (let index = 0; index < productNames.length; index++) {
productIcons.push('app.media.product0${index+1}.jpg');
}
const items: Array<ShoppingCartCellVMV2> = []
for (let index = 0; index < 10; index++) {
let nameRandom: number = this.generateRandomInRange(0, 4)
let iconRandom: number = this.generateRandomInRange(1, 9)
let model: ProductItemV2 = {
productName: productNames[nameRandom],
productIcon: 'app.media.product0' + iconRandom,
productDesc: '暂时没有',
productPrice: '1.1' + index
}
items.push(new ShoppingCartCellVMV2(model))
}
this.cellVMArr = items
}
}
模型层
/**
* 商品模型
*/
interface ProductItemV2 {
/**
* 商品名称
*/
productName?: string
/**
* 商品图片
*/
productIcon?: string
/**
* 商品描述
*/
productDesc?: string
/**
* 商品价格
*/
productPrice?: string
}
自定义单选view
/**
* 针对 UgCheckBoxV2 定义的抽象类
*/
abstract class UgCheckBoxV2Type {
selected: boolean = false
}
/**
* 自定义单选view
*/
@ComponentV2
struct UgCheckBoxV2 {
@Param @Require selectedItem: UgCheckBoxV2Type
@Param title: string = ''
@Event didClick: (sel: boolean) => void
build() {
Row() {
Text() {
SymbolSpan(
$r(this.selectedItem.selected ? 'sys.symbol.smallcircle_filled_circle' : 'sys.symbol.circle')
)
}
.onClick((e) => {
this.selectedItem.selected = !this.selectedItem.selected
if (this.didClick != undefined) {
this.didClick!(this.selectedItem.selected)
}
})
.padding({ right: 8 })
if (this.title != undefined) {
Text(this.title)
}
}
}
}
商品cell层
@ComponentV2
struct ShoppingCartCellV2 {
/**
* cell对应的vm
*/
@Param @Require cellVM: ShoppingCartCellVMV2
/**
* 页面的vm
*/
@Param @Require pageVM: ShoppingCartVMV2
build() {
Row() {
RelativeContainer() {
UgCheckBoxV2({
selectedItem: this.cellVM, didClick: (e) => {
this.cellVM.selected = e;
this.pageVM.checkSelected();
}
})
.alignRules({
center: { anchor: '__container__', align: VerticalAlign.Center },
})
.id("row1")
.margin({ right: 8 })
// 商品图片
Image($r(this.cellVM.getProductIcon()))
.width(50)
.height(50)
.border({ radius: 8 })
.alignRules({
left: { anchor: 'row1', align: HorizontalAlign.End },
center: { anchor: '__container__', align: VerticalAlign.Center },
})
.id("row2")
Column() {
Text(this.cellVM.getProductName())
.maxLines(1)
.fontSize(15)
.fontWeight(FontWeight.Medium)
.textOverflow({ overflow: TextOverflow.Ellipsis })
.margin({ bottom: 4 })
Text(this.cellVM.getProductDesc())
.maxLines(2)
.font({ size: 14, weight: FontWeight.Regular })
.fontColor(Color.Gray)
.textOverflow({ overflow: TextOverflow.Ellipsis })
.margin({ bottom: 8 })
Flex({ alignItems: ItemAlign.Center }) {
Text(this.cellVM.getPrice())
.font({ size: 16, weight: FontWeight.Medium })
.fontColor(Color.Red)
Blank()
// 计数器组建
Counter() {
Text(this.cellVM.buyCount.toString())
}
.onInc(() => {
this.cellVM.buyCount++;
this.pageVM.checkSelected();
})
.onDec(() => {
if (this.cellVM.buyCount <= 1) {
return
}
this.cellVM.buyCount--;
this.pageVM.checkSelected();
})
}
}
.alignItems(HorizontalAlign.Start)
.margin({ left: 8 })
.alignRules({
left: { anchor: 'row2', align: HorizontalAlign.End },
right: { anchor: '__container__', align: HorizontalAlign.End },
center: { anchor: '__container__', align: VerticalAlign.Center },
})
}
}
.width('94%')
.height(120)
.margin('3%')
.padding({
left: 5,
right: 12,
top: 8,
bottom: 8
})
.border({ color: '#FFE6E6E6', width: 1.0, radius: 12 })
}
}
商品cellVM层
@ObservedV2
class ShoppingCartCellVMV2 {
/**
* moc 网络请求获取的模型
*/
private item: ProductItemV2
/**
* 购买的个数
*/
@Trace buyCount: number = 1
/**
* 是否选中
* checkBox层需要监听这个属性,需要@Trace
*/
@Trace selected: boolean = false
/**
* 通过网咯请求模型构建vm
*/
constructor(item?: ProductItemV2) {
this.item = item ?? {}
}
/**
* 获取商品名称
*/
getPriceNumber(): string {
return this.item?.productPrice ?? '0'
}
/**
* 获取价格
*/
getPrice(): string {
return '¥' + this.item?.productPrice ?? '';
}
/**
* 获取名字
*/
getProductName(): string {
return this.item?.productName ?? '';
}
/**
* 获取描述
*/
getProductDesc(): string {
return this.item?.productDesc ?? '';
}
/**
* 获取Icon
*/
getProductIcon(): string {
return this.item?.productIcon ?? '';
}
}
- 效果如下: