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

前言

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

TypeScript 复制代码
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、arri = value等
class对象 仅整体赋值(第一层),属性修改不触发刷新
ArrayList等容器类 无法观测 API 调用带来的变化

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

TypeScript 复制代码
 @State numItems: Array<string> = new Array();

或者如下:

TypeScript 复制代码
@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 限制了存储位置和属性,不支持自定义属性和方法 ,这进一步限制了框架对其进行代理包装的可能性。

我们简单做下对比:

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

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

以后应当这么做

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

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

TypeScript 复制代码
  @Observed
  class TestBean {
    name?: string

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

第二步,视图抽取子组件

TypeScript 复制代码
@Component
struct TextView {
    @ObjectLink item: TestBean

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

第三步,改为子组件

TypeScript 复制代码
@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一般配套使用。

TypeScript 复制代码
@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 绑定、无跨线程需求的情况下,且需要频繁按索引随机访问,还是很推荐使用的,如下代码所示:

TypeScript 复制代码
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呢?

相关推荐
威哥爱编程44 分钟前
HarmonyOS 7 数字盾 2.0 实战:可信数字签名、可信 UI 确认与可信输入
harmonyos
hai_android1 小时前
深入理解 Java 的四种引用:强引用、软引用、弱引用、虚引用
android·kotlin·android jetpack
恋猫de小郭1 小时前
Flutter Golden Tests:给 AI Agent 的 UI 测试系统
android·前端·flutter
知昂七昂1 小时前
AOSP编译:敲下 `lunch xxx-userdebug`,你知道系统偷偷干了什么吗?
android
威哥爱编程1 小时前
HarmonyOS 7 数字身份 DID 实战:TEE 颁发、本人同意、最小化出示
华为·harmonyos·arkts
威哥爱编程1 小时前
HarmonyOS 7 平行视界进阶实战:配置写好了,为什么一上真机就崩?
harmonyos
千里马学框架5 天前
一起学 Android 14:ShellTransition 屏幕旋转过程深度剖析
android·智能手机·性能优化·framework·性能·屏幕旋转·rotation
美狐美颜SDK开放平台5 天前
开发直播APP时如何接入视频美颜SDK?开发流程与注意事项
android·人工智能·计算机视觉·音视频·直播美颜sdk
AFinalStone5 天前
Android7 SystemUI源码解析(七)Keyguard锁屏模块深度解析
android·systemui