ArkUI-状态管理最佳实践

概述

在声明式UI编程范式中,UI是应用程序状态的函数,应用程序状态的修改会更新响应的UI界面。ArkUI采用了MVVM模式。
在这里插入图片描述

ArkUI提供了一系列装饰器实现ViewModel的能力,当自定义组件内变量被装饰器装饰时变为状态变量,状态变量的改变会触发UI渲染刷新。

在ArkUI的开发过程中,如果没有选择合适的装饰器或合理的状态控制更新范围,可能会导致以下问题:

  • 状态和UI的不一致,如同一状态的界面元素展示的UI不同,或UI界面展示的不是最新的状态。
  • 非必要的UI视图刷新,如只修改局部组件状态时导致组件所在页面的整体刷新。

合理选择装饰器

状态变量的管理有一定的开销,应在合理场景使用,我们应从以下几个方面尽可能的优化状态变量的使用:

  • 避免不必要的状态变量的使用
  • 删除冗余的状态变量标记
  • 使用临时变量替换状态变量,状态变量的变化会引起UI刷新,当我们需要对状态变量进行连续计算等操作时,应使用临时变量进行替换,计算完成后再修改状态变量。
  • 最小化共享范围,组件内独享使用@State,组件间共享,ArkUI提供了了@State+@Prop@State+@Link@State+@Observed+@ObjectLink@Provide+@ConsumeAppStorageLocalStorage六种装饰器组合以解决不同范围内的组件间状态共享。按照共享范围能力从小到大,各装饰器组合的共享范围能力和生命周期如下:
    1. @State+@Prop、@State+@Link、@State+@Observed+@ObjectLink:三者的共享范围为从@State所在的组件开始,到@Prop/@Link/ObjectLink所在组件的整条路径,路径上所有的中间组件通过@Prop/@Link/@ObjectLink都可以共享同一个状态。@State修饰的状态和其所属的自定义组件共享生命周期,在组件内定义时创建,组件销毁时被回收。@Link装饰的变量和其所属的自定义组件共享生命周期。@ObjectLink装饰的变量和其所属的自定义组件共享生命周期。
    2. @Provide+@Consume:状态共享范围是以@Provide所在组件为祖先节点的整棵子树,子树上的任意后代组件通过@Consume都可以共享同一个状态。@Provide修饰的变量与其所属的组件绑定,在组件内定义时被创建,在组件销毁时被回收。
    3. LocalStorage:共享范围为UIAbility内以页面为单位的不同组件树间的共享。存储在LocalStorage中的状态的生命周期与LocalStorage绑定。LocalStorage的声明周期由应用程序决定,当应用释放最后一个指向LocalStorage的引用时,LocalStorage被垃圾回收。
    4. AppStorage:共享范围是应用全局。AppStorage与应用的进程绑定,由UI框架在应用程序启动时创建,当应用进程终止,AppStorage被回收。存储在AppStorage中的状态的生命周期与LocalStorage绑定。
  • 减少不必要的层层传递
  • 按照状态复杂度选择装饰器
    在这里插入图片描述
    1. @State+@Prop组合方案:
      • @Prop装饰器支持接收Object、class、string、number、boolean、enum类型,以及这些类型的数组。
      • @Prop装饰的变量是对父组件传入状态值的深拷贝,当@Prop装饰器装饰的变量为复杂Object、class或其类型数组时,会增加状态创建时间以及占用大量内存。
      • @Prop装饰的变量和父组件是单向绑定的关系。当父组件数据源发生变化时,接收该数据源的@Prop所在组件的实例会重新渲染。 当该组件内被@Prop装饰的变量被修改时,父组件数据源不会变化,父组件实例也不会重新渲染。
    2. @State+@Link组合方案:
      • @Link装饰器支持接收Object、class、string、number、boolean、enum类型,以及这些类型的数组。
      • @Link装饰器修饰的变量是对父组件传入状态的引用的拷贝,两者指向同一个地址。当状态是简单数据类型或简单Object类型时,@Link和@Prop在状态创建时间和内存的占用方面区别不大。当状态为复杂的Object、class或其类型数组时,@Link相较@Prop能明显减少状态创建时间和内存的占用。
      • @Link装饰器的变量和父组件是双向绑定的关系。当父组件数据源发生变化时,接收该数据源的@Link所在组件的实例会重新渲染。 当该组件内被@Link装饰的变量被修改时,父组件数据源会同步修改,父组件实例也会重新渲染。
    3. @State+@Observed+@ObjectLink组合方案:
      • @ObjectLink只支持接收被@Observed装饰的class实例及继承Date或者Array的class实例。
      • @ObjectLink装饰的变量是只读的,不支持对状态重新赋值。
      • @ObjectLink必须配合@Observed使用,它的设计是为了解决对嵌套类对象属性变化的监听,如需要观察对象数组中单个数据项的属性值变化,或嵌套对象的对象类型属性的子属性变化。

结合三个方案的特性,在选择时有如下建议:

  • 需要观察嵌套类对象的深层属性变化的场景,选择@State+@Observed+@ObjectLink。
  • 状态是复杂对象、类或其类型数组的场景,选择@State+@Link。
  • 状态是简单数据类型时,使用@State+@Link和@State+@Prop均可。在功能层面上,依据@Prop单向绑定的特性,@State+@Prop适合用于非实时修改的场景,如编辑电话薄联系人信息时,展示编辑界面的子组件信息的修改要求不实时同步回父组件,需要等到编辑完成后点击“确认”按钮时才会以事件驱动的方式修改父组件的状态。依据@Link双向绑定的特性,@State+@Link适合用于实时修改的场景,如组件嵌套时的滚动条同步。

使用监听和订阅精准控制组件刷新

多个组件依赖对象中的不同属性时,直接关联该对象会出现改变任一属性所有组件都刷新的现象,可以通过将类中的属性拆分组合成新类的方式精准控制组件刷新。

@Watch装饰器监听数据源

在组件中使用@Watch装饰器监听数据源,当数据变化时执行业务逻辑,确保只有满足条件的组件进行刷新。

@Entry
@Component
struct UseWatchListener {
  @State currentIndex: number = 0; // 当前选中的列表项下标
  private listData: string[] = [];

  aboutToAppear(): void {
    for (let i = 0; i < 10; i++) {
      this.listData.push(`组件 ${i}`);
    }
  }

  build() {
    Row() {
      Column() {
        List() {
          ForEach(this.listData, (item: string, index: number) => {
            ListItem() {
              ListItemComponent({ item: item, index: index, currentIndex: this.currentIndex })
            }
          })
        }
        .height('100%')
        .width('100%')
        .alignListItem(ListItemAlign.Center)
      }
      .width('100%')
    }
    .height('100%')
  }
}

@Component
struct ListItemComponent {
  @Prop item: string;
  @Prop index: number; // 列表项的下标
  @Link @Watch('onCurrentIndexUpdate') currentIndex: number;
  @State color: Color = Math.abs(this.index - this.currentIndex) <= 1 ? Color.Red : Color.Blue;

  isRender(): number {
    console.info(`ListItemComponent ${this.index} Text is rendered`);
    return 50;
  }

  onCurrentIndexUpdate() {
    // 根据当前列表项下标index与currentIndex的差值来动态修改color的值
    this.color = Math.abs(this.index - this.currentIndex) <= 1 ? Color.Red : Color.Blue;
  }

  build() {
    Column() {
      Text(this.item)
        .fontSize(this.isRender())
        .fontColor(this.color)
        .onClick(() => {
          this.currentIndex = this.index;
        })
    }
  }
}

上述代码中,ListItemComponent组件中的状态变量currentIndex使用@Watch装饰,Text组件直接关联新的状态变量color。当currentIndex发生变化时,会触发onCurrentIndexUpdate方法,在其中将表达式的运算结果赋值给状态变量color。只有color的值发生变化时,Text组件才会重新渲染,运行效果图如下:

请添加图片描述
被依赖的数据源仅在父子或兄弟关系的组件中传递时,可以参考上述示例,使用@State/@Link/@Watch装饰器进行状态管理,实现组件的精准刷新。

当组件关系层级较多但都归属于同一个确定的组件树时,推荐使用@Provide/@Consume传递数据,使用@Watch装饰器监听数据变化,在监听回调中执行业务逻辑。

使用自定义事件发布订阅

当组件关系复杂或跨越层级过多时,推荐使用EventHub或者Emitter自定义事件发布订阅的方式。当数据源改变时发布事件,依赖该数据源的组件通过订阅事件来获取数据源的改变,完成业务逻辑的处理,从而实现组件的精准刷新。

  • 13
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值