ArkUI-MVVM(购物车V2版本)

购物车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 ?? '';
  }
}
  • 效果如下:
    请添加图片描述
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值