前言
有同学遇到了一个问题,他是使用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呢?