鸿蒙开发:ArrayList,可不会让UI更新哦

137 阅读6分钟

前言

有同学遇到了一个问题,他是使用ArrayList来存储的数据,在动态给集合添加数据后,UI并未更新,代码如下:


import { ArrayList } from '@kit.ArkTS';

@Entry
@Component
struct Index {
  @State numItems: ArrayList<string> = new ArrayList();

  aboutToAppear(): void {
    this.numItems.add("数据一")
  }

  build() {
    Column() {
      ForEach(this.numItems.convertToArray(), (item: string) => {
        Text(item)
          .height(30)
      })
      Button("添加数据")
        .onClick(() => {
          this.numItems.add("数据二")
        })
    }.width("100%")
    .alignItems(HorizontalAlign.Center)
  }
}

貌似以上的代码看着没太大问题,ForEach中由于支持的是Array类型,便使用convertToArray()方法做了转换,但是,在点击添加数据按钮之后,UI并没有按照理想的状态去增加数据。

这就是,我们需要了解的一个知识点,不要用Android的那一套思维来开发鸿蒙,也许在Android中,ArrayList非常常见也常用,但是,在鸿蒙开发中,牵扯到UI更新,却不能使用ArrayList,这一点,对于初学者务必牢记。

首先我们要知道,ArrayList是@kit.ArkTS提供的线性容器类,并非原生Array,V1装饰器下的@State装饰器,只能观测原生 Array的增删改等操作,对于ArrayList的相关的API变化,如add(),是无法观测的。

目前@State可以观测变化如下表:

类型可观测的变化
基础类型(number、string 等)值变化
原生Array整体赋值、push、pop、splice、arr[i] = value等
class对象仅整体赋值(第一层),属性修改不触发刷新
ArrayList等容器类无法观测 API 调用带来的变化

所以,如果只是基础类型的数组观测,可以直接使用原生的数组就可以,比如:

 @State numItems: Array<string> = new Array();

或者如下:

@State numItems: string[] = [];

不能在继续使用 ArrayList。

为什么不能观测ArrayList里的API调用

一句话概括:根本原因在于观测机制Proxy代理与ArrayList的底层实现方式不兼容。

状态管理的观测原理

鸿蒙状态管理框架,无论是V1版本下的的@State还是V2版本下的@Local,他们的核心观测机制是Proxy代理,当变量被状态装饰器装饰时,框架会在数据对象外层包裹一层代理对象,代理会拦截对数据的属性访问和修改操作,从而感知变化并触发 UI 刷新。

我们以原生Array为例,它其中的push、pop、splice 等方法操作,其实是 JavaScript 引擎层面的属性,如length、索引元素,Proxy可以拦截这些操作,因此@State和@Local能观测到变化。

ArrayList 为什么无法被代理拦截

ArrayList是@kit.ArkTS提供的容器类,其关键特征是使用静态语言(C++)实现,这意味着,第一,数据存储在原生内存中,ArrayList的内部元素存储在 C++ 层的连续内存空间中,而不是JavaScript 可访问的对象属性。Proxy只能拦截 JavaScript 层面的属性操作,无法感知原生内存中的数据变化;第二,API 方法是原生绑定,ArrayList的add()、remove()、insert()等方法直接调用 C++ 实现,方法执行过程中的数据增删改发生在原生层面,完全绕过了Proxy的拦截能力;第三,不支持自定义属性和方法:ArrayLis 限制了存储位置和属性,不支持自定义属性和方法 ,这进一步限制了框架对其进行代理包装的可能性。

我们简单做下对比:

对比项原生ArrayArrayList
实现层面JavaScript 引擎内置C++ 静态语言实现
数据存储JavaScript 对象属性(索引、length)原生内存空间
API 执行JavaScript 层面,Proxy 可拦截原生层面,Proxy 无法拦截
自定义属性支持不支持
Proxy 代理可正常包装拦截无法有效拦截

一句话总结:ArrayList作为 C++ 静态语言实现的容器类,其数据存储和 API 调用均在原生层面完成,Proxy 无法介入拦截,因此框架感知不到add()、remove() 等调用带来的数据变化,UI 自然不会刷新。

以后应当这么做

前边在前言中已经诉说了基本类型数组的观测,这里就不再过多赘述,如果在实际的开发中遇到的不是基本类型的数组,比如对象数组,V1装饰器下,需要结合@Observed/@ObjectLink装饰器来使用。

第一步,给对象加上@Observed装饰器

  @Observed
  class TestBean {
    name?: string

    constructor(name: string) {
      this.name = name
    }
  }

第二步,视图抽取子组件

@Component
struct TextView {
    @ObjectLink item: TestBean

    build() {
      Text(this.item.name)
        .height(30)
    }
  }

第三步,改为子组件

@Entry
@Component
struct Index {
  @State items: TestBean[] = [
    new TestBean("条目一"),
    new TestBean("条目二"),
    new TestBean("条目三")]

  build() {
    Column() {
      ForEach(this.items, (item: TestBean) => {
        TextView({ item: item })
      })
      Button("添加数据")
        .onClick(() => {
          this.items.push(new TestBean("条目四"))
        })
    }.width("100%")
    .alignItems(HorizontalAlign.Center)
  }
}

如果是使用的V2装饰器,那对象数组更新就比较简单了,直接使用@ObservedV2即可,如果你想继续类属性变化观测,可以给对象中的属性,设置@Trace,@ObservedV2和@Trace一般配套使用。

@Entry
@ComponentV2
struct Index {
  @Local items: TestBean[] = [
    new TestBean("条目一"),
    new TestBean("条目二"),
    new TestBean("条目三")]

  build() {
    Column() {
      ForEach(this.items, (item: TestBean) => {
        Text(item.name)
          .height(30)
      })
      Button("添加数据")
        .onClick(() => {
          this.items.push(new TestBean("条目四"))
        })
    }.width("100%")
    .alignItems(HorizontalAlign.Center)
  }
}

@ObservedV2
class TestBean {
  @Trace name?: string

  constructor(name: string) {
    this.name = name
  }
}

还推荐使用ArrayList吗

在UI更新操作中是不能使用的,但是毫不影响其他非UI的操作。

ArrayList 的适用与不适用场景

场景是否推荐 ArrayList推荐替代方案
频繁按索引随机访问元素推荐—
增删操作频率较低的有序数据管理推荐—
UI 状态管理(@State/@Local驱动刷新)不推荐原生Array(T[])
需要 API 级观测 + 并发安全不推荐collections.Array+ makeObserved
跨线程传递数据不支持collections.Array
频繁插入删除元素不推荐LinkedList
两端频繁增删元素不推荐Deque

比如,纯数据逻辑层,无 UI 绑定、无跨线程需求的情况下,且需要频繁按索引随机访问,还是很推荐使用的,如下代码所示:

import { ArrayList } from '@kit.ArkTS';

class DataProcessor {
  private data: ArrayList<string> = new ArrayList();

  // 频繁按索引读取,ArrayList 的随机访问效率高
  getItem(index: number): string {
    return this.data[index];
  }

  addItem(item: string): void {
    this.data.add(item);
  }
}

相关总结

在实际开发中,大多数涉及 UI 的场景应优先使用原生Array,它语法简洁、状态管理兼容性好、ForEach直接支持,而ArrayList适合用在,纯数据逻辑层中需要频繁随机访问的场景,所以,大家一定要区分开来。

抛出一个疑问,如果我硬要使用ArrayList,那么如何来更新UI呢?