购物车MVVM案例
这个案例需要了解一些状态管理的知识,比如@Prop,@Observed、ObjectLink等,下面我们就先来了解本案例用到的,如下:
@State装饰器:组件内状态
官方资料: https://developer.huawei.com/consumer/cn/doc/harmonyos-guides-V2/arkts-state-0000001474017162-V2
@State装饰的变量,或称为状态变量,一旦变量拥有了状态属性,就和自定义组件的渲染绑定起来。当状态改变时,UI会发生对应的渲染改变。
在状态变量相关装饰器中,@State是最基础的,使变量拥有状态属性的装饰器,它也是大部分状态变量的数据源。
概述
@State装饰的变量,与声明式范式中的其他被装饰变量一样,是私有的,只能从组件内部访问,在声明时必须指定其类型和本地初始化。初始化也可选择使用命名参数机制从父组件完成初始化。
@State装饰的变量拥有以下特点
- @State装饰的变量与子组件中的@Prop装饰变量之间建立单向数据同步,与@Link、@ObjectLink装饰变量之间建立双向数据同步。
- @State装饰的变量生命周期与其所属自定义组件的生命周期相同。
@Prop装饰器:父子单向同步
官方资料: https://developer.huawei.com/consumer/cn/doc/harmonyos-guides-V2/arkts-prop-0000001473537702-V2#section614118685518
@Prop装饰的变量可以和父组件建立单向的同步关系。@Prop装饰的变量是可变的,但是变化不会同步回其父组件。
概述
-
@Prop装饰的变量和父组件建立单向的同步关系:
-
@Prop变量允许在本地修改,但修改后的变化不会同步回父组件。
当父组件中的数据源更改时,与之相关的@Prop装饰的变量都会自动更新。如果子组件已经在本地修改了@Prop装饰的相关变量值,而在父组件中对应的@State装饰的变量被修改后,子组件本地修改的@Prop装饰的相关变量值将被覆盖。
限制条件:
- @Prop装饰器不能在@Entry装饰的自定义组件中使用。
@Observed装饰器和@ObjectLink装饰器:嵌套类对象属性变化
官方资料:https://developer.huawei.com/consumer/cn/doc/harmonyos-guides-V2/arkts-observed-and-objectlink-0000001473697338-V2
上文所述的装饰器仅能观察到第一层的变化,但是在实际应用开发中,应用会根据开发需要,封装自己的数据模型。对于多层嵌套的情况,比如二维数组,或者数组项class,或者class的属性是class,他们的第二层的属性变化是无法观察到的。这就引出了@Observed/@ObjectLink装饰器。
概述
@ObjectLink和@Observed类装饰器用于在涉及嵌套对象或数组的场景中进行双向数据同步:
-
被@Observed装饰的类,可以被观察到属性的变化;
-
子组件中@ObjectLink装饰器装饰的状态变量用于接收@Observed装饰的类的实例,和父组件中对应的状态变量建立双向数据绑定。这个实例可以是数组中的被@Observed装饰的项,或者是class object中的属性,这个属性同样也需要被@Observed装饰。
-
单独使用@Observed是没有任何作用的,需要搭配@ObjectLink或者@Prop使用
限制条件
- 使用@Observed装饰class会改变class原始的原型链,@Observed和其他类装饰器装饰同一个class可能会带来问题。
- @ObjectLink装饰器不能在@Entry装饰的自定义组件中使用。
购物车详细代码
- 购物车页面
@Entry
@Component
struct ShoppingCart {
@State vm: ShoppingCartVM = new ShoppingCartVM()
onPageShow(): void {
this.vm.loadData()
}
//
build() {
Flex({ direction: FlexDirection.Column }) {
List() {
ForEach(this.vm.cellVMArr, (item: ShoppingCartCellVM) => {
ListItem() {
ShoppingCartCell({ cellVM: item, pageVM: this.vm })
}
})
}
Row() {
UgCheckBox({
selected: this.vm.selectedAll, 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)
}
}
}
- 点击选择的view
@Component
struct UgCheckBox {
@Prop selected: boolean
title?: string
didClick?: (sel: boolean) => void
build() {
Row() {
Text() {
SymbolSpan(
$r(this.selected ? 'sys.symbol.smallcircle_filled_circle' : 'sys.symbol.circle')
)
}
.onClick((e) => {
this.selected = !this.selected;
if (this.didClick != undefined) {
this.didClick!(this.selected)
}
})
.padding({ right: 8 })
if (this.title != undefined) {
Text(this.title)
}
}
}
}
- 商品模型
interface ProductItem {
/// 商品名称
productName?: string
/// 商品图片
productIcon?: string
/// 商品描述
productDesc?: string
/// 商品价格
productPrice?: string
}
- 购物车页面的VM
@Observed
class ShoppingCartVM {
/// 是否全选
selectedAll: boolean = false
/// 选择的商品个数
selectedCount: number = 0
/// 总价
selectedAllPrice: string = '0.0'
/// 构造方法
constructor() {
this.loadData()
}
/// moc 请求获取的商品模型数组
cellVMArr: ShoppingCartCellVM[] = []
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.selectedAll = 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');
}
for (let index = 0; index < 10; index++) {
let nameRandom: number = this.generateRandomInRange(0, 4)
let iconRandom: number = this.generateRandomInRange(1, 9)
let model: ProductItem = {
productName: productNames[nameRandom],
productIcon: 'app.media.product0' + iconRandom,
productDesc: '暂时没有',
productPrice: '1.1' + index
}
// this.productModels?.push(model)
this.cellVMArr.push(new ShoppingCartCellVM(model))
}
}
}
- 购物车的卡片Cell
@Component
struct ShoppingCartCell {
/// 这里必须使用 ObjectLink
/// 修改cellVM的同时需要修改源数据
@ObjectLink cellVM: ShoppingCartCellVM
@ObjectLink pageVM: ShoppingCartVM
build() {
Row() {
RelativeContainer() {
UgCheckBox({ selected: this.cellVM.selected, 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('我是详情我是详情我是情我是详情我是情我是详情我是情我是详情我是是详情我是情我是详情我是')
.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: 'row1', 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 })
}
}
- 购物车卡片cell对应的cellVM
@Observed
class ShoppingCartCellVM {
private item: ProductItem
/// 购买的个数
buyCount: number = 1
/// 是否选中
selected: boolean = false
constructor(item: ProductItem) {
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 ?? '';
}
}
- 效果如下: