HarmonyOS 鸿蒙Next中实现多层嵌套数据的多选单选处理
HarmonyOS 鸿蒙Next中实现多层嵌套数据的多选单选处理 一、需求:对一个对象嵌套一级数组,一级数组嵌套二级对象,二级对象嵌套二级数组,三层嵌套数据进行单选和多选(类似筛选效果)的UI显示效果,并根据点击对象,自动滑动到对应的数据节点;
二、实现思路对比和差异(不喜勿喷):本人原是iOS开发,逻辑是通过实体类对象记录和控制数据的选中状态、单选和多选,而鸿蒙这里,查询官方api后比较容易采纳@Observe和@ObjectLink装饰器进行对象数据属性的变化监听,查询并验证官方demo(链接见https://developer.huawei.com/consumer/cn/doc/harmonyos-guides-V13/arkts-observed-and-objectlink-V13)后,无法实现对应的效果,反复实验,总的结果就是一条:一看就会,一写就错,无法爬坑,只能另起炉灶使用@Provide和@Consume装饰器来实现,利用@Provide装饰的属性,可以被子孙组件共享数据的特性,使用
[@Provide](/user/Provide) selects: Map<string, string | string[]> = new Map(),
存储对应实体类对象唯一的id来记录选择的对象,然后根据selects来判断是否存在该对象的id,实现属性变化的监听
- 示例代码
import { ZRoute } from "@hzw/zrouter"
import { LogUtil, ObjectUtil, StrUtil } from "@pura/harmony-utils"
import { TitleBar } from "../../common/TitleBar"
import { Rts } from "../../router/Rts"
class A {
id: string = ''
name: string = ''
bs: B[] = []
constructor(id: string, name: string, issingle: boolean = true) {
this.id = id
this.name = name
this.bs = [new B(id + '11', id + 'B_1'), new B(id + '12', id + 'B_2'), new B(id + '13', id + 'B_3', issingle)]
}
}
class B {
id: string = ''
name: string = ''
issingle: boolean = true
cs: C[] = []
constructor(id: string, name: string, issingle: boolean = true) {
this.id = id
this.name = name
this.issingle = issingle
this.cs = [new C(id, '111', 'C_1'), new C(id, '112', 'C_2'), new C(id, '113', 'C_3'), new C(id, '114', 'C_4')]
}
}
class C {
bid: string = ''
cid: string = ''
name: string = ''
constructor(bid: string, cid: string, name: string) {
this.bid = bid
this.cid = cid
this.name = name
}
}
@Component
struct CSItem {
@Prop item: C;
[@Consume](/user/Consume) selects: Map<string, string | string[]>
build() {
Text(this.item.name)
.borderRadius(12)
.height(44)
.textAlign(TextAlign.Center)
.onClick(() => {
if (this.selects.get(this.item.bid) === this.item.cid) {
this.selects.delete(this.item.bid)
} else {
this.selects.set(this.item.bid, this.item.cid)
}
})
.width('90%')
.backgroundColor(this.selects.get(this.item.bid) == this.item.cid ? Color.Orange : Color.Yellow)
}
}
@Component
struct CMItem {
@Prop item: C;
[@Consume](/user/Consume) selects: Map<string, string | string[]>
build() {
Text(this.item.name)
.borderRadius(12)
.height(44)
.textAlign(TextAlign.Center)
.onClick(() => {
try {
let length = (this.selects.get(this.item.bid) as string[]).filter((v) => v === this.item.cid).length
let index = (this.selects.get(this.item.bid) as string[]).indexOf(this.item.cid)
if (length === 0) {
this.selects.set(this.item.bid, [this.item.cid])
} else if (length === 1) {
this.selects.delete(this.item.bid)
} else {
(this.selects.get(this.item.bid) as string[]).slice(index, 1)
}
} catch (e) {
}
})
.width('90%')
.backgroundColor(this.selects.get(this.item.bid) == this.item.cid ? Color.Orange : Color.Yellow)
}
}
@Component
struct BItem {
@Prop item: B;
[@Consume](/user/Consume) selects: Map<string, string | string[]>
build() {
Column() {
Text(this.item.name)
.width('100%')
.height(44)
.textAlign(TextAlign.Center)
.onClick(() => {
this.selects.delete(this.item.id)
})
List({ space: 12 }) {
ForEach(this.item.cs, (item: C) => {
if (this.item.issingle) {
CSItem({
item: item,
})
} else {
CMItem({
item: item,
})
}
})
}
.alignListItem(ListItemAlign.Center)
.visibility(this.selects.has(this.item.id) && this.item.issingle ? Visibility.None : Visibility.Visible)
}
.width('90%')
.borderRadius(12)
.backgroundColor(this.selects.has(this.item.id) ? Color.Red : Color.Grey)
}
}
@ZRoute({ name: Rts.result, useTemplate: true })
@Preview
@Component
export struct Result {
private scroller: Scroller = new Scroller()
private itemAs: A[] =
[new A('1', 'A-1'), new A('2', 'A-2'), new A('3', 'A-3'), new A('4', 'A-4'),
new A('5', 'A-5')]
[@Provide](/user/Provide) selects: Map<string, string | string[]> = new Map()
build() {
Column() {
TitleBar({ title: '结果提交' })
Row() {
Button('1B_2').onClick(() => {
this.scroller.scrollToIndex(1)
})
Button('2B_1').onClick(() => {
this.scroller.scrollToIndex(3)
})
Button('2B_2').onClick(() => {
this.scroller.scrollToIndex(4)
})
Button('4B_2').onClick(() => {
this.scroller.scrollToIndex(10)
})
}
List({ space: 12, scroller: this.scroller }) {
ForEach(this.itemAs, (itema: A) => {
//this.AItem(item)
//List({ space: 12, scroller: this.scroller_1 }) {
ForEach(itema.bs, (item: B, index: number) => {
ListItem() {
Column() {
Text(itema.name)
.width('100%')
.height(44)
.visibility(index === 0 ? Visibility.Visible : Visibility.None)
.textAlign(TextAlign.Center)
.backgroundColor(Color.White)
.onClick(() => {
let a: string[] = []
this.selects.forEach((key, value) => {
a.push(value)
})
LogUtil.error('==========' + a.join(','))
})
BItem({
item: item,
})
}
}
})
// }
// .alignListItem(ListItemAlign.Center)
})
}
.alignListItem(ListItemAlign.Center)
}
.width('100%')
.height('100%')
.backgroundColor("#F7F7F7")
.expandSafeArea()
}
@Builder
AItem(item: A) {
Column() {
Text(item.name)
.width('100%')
.height(44)
.textAlign(TextAlign.Center)
.onClick(() => {
LogUtil.error('==========' + this.selects.size)
let a: string[] = []
this.selects.forEach((key, value) => {
a.push(value)
})
})
//List({ space: 12, scroller: this.scroller_1 }) {
ForEach(item.bs, (item: B) => {
BItem({
item: item,
})
})
// }
// .alignListItem(ListItemAlign.Center)
}
.width('90%')
.borderRadius(12)
.backgroundColor(Color.Pink)
}
}
看demo,运行起来查看可能比较直观,如有疑问可以再私信我,仅供参考
更多关于HarmonyOS 鸿蒙Next中实现多层嵌套数据的多选单选处理的实战教程也可以访问 https://www.itying.com/category-93-b0.html
在HarmonyOS Next中,处理多层嵌套数据的多选和单选,可以使用ArkUI的@State和@Link装饰器管理状态。通过递归组件或ForEach循环渲染嵌套结构,结合CheckboxGroup或自定义选择逻辑实现。多选时维护选中项ID集合;单选时使用变量记录当前选中项。数据变更通过状态管理自动更新UI。
更多关于HarmonyOS 鸿蒙Next中实现多层嵌套数据的多选单选处理的实战系列教程也可以访问 https://www.itying.com/category-93-b0.html
你的思路是可行的,使用 @Provide 和 @Consume 在多层嵌套组件间共享一个状态管理 Map 来记录选中项,这确实绕过了 @Observed 和 @ObjectLink 在处理复杂嵌套对象时的某些限制。
针对你的实现,有几个关键点可以优化和明确:
-
状态管理的核心:你使用
Map<string, string | string[]>作为核心状态容器是合理的。Key 通常是父级(如 B 的id),Value 是子级选中项的标识(单选string,多选string[])。这实现了状态与数据模型的解耦。 -
@Provide/@Consume的适用性:在这个场景下,它们比@Observed/@ObjectLink更直接。后者要求对每个嵌套层级的类使用@Observed装饰,并在子组件中@ObjectLink该类的实例,才能实现深度的、可观测的属性变更。对于你已有的数据模型(A, B, C),改造起来可能更繁琐。@Provide/@Consume提供了跨层级的、类似“全局上下文”的状态共享,更符合你“集中管理ID”的思路。 -
代码优化点:
- CMItem 组件逻辑:多选操作逻辑可以更简洁。直接获取当前数组,添加或删除项目,然后
set回 Map。.onClick(() => { let current = (this.selects.get(this.item.bid) as string[]) || []; const index = current.indexOf(this.item.cid); if (index > -1) { current.splice(index, 1); // 取消选中 } else { current.push(this.item.cid); // 选中 } // 如果数组为空,可以选择 delete 键,也可以 set 为空数组,取决于你的业务逻辑 this.selects.set(this.item.bid, current.length > 0 ? current : []); }) - CMItem 背景色判断:
.backgroundColor(this.selects.get(this.item.bid) == this.item.cid ? ... : ...)这行代码在多选模式下 (string[]) 与string比较可能不准确。应使用数组的includes方法:.backgroundColor((this.selects.get(this.item.bid) as string[])?.includes(this.item.cid) ? Color.Orange : Color.Yellow) - 类型安全:在
CMItem和CSItem中,对this.selects.get()的结果进行as string[]或as string的类型断言存在风险。建议在 Map 的 Value 类型定义上更精确,或者在使用前进行类型检查。
- CMItem 组件逻辑:多选操作逻辑可以更简洁。直接获取当前数组,添加或删除项目,然后
-
自动滑动到节点:你示例中通过
Scroller的scrollToIndex实现。这需要你预先计算好每个BItem(或目标项)在扁平化列表中的索引。你的示例通过硬编码索引演示了原理,在实际应用中,需要根据数据结构和选中项动态计算这个索引。
总结:你当前的架构是有效的。它利用鸿蒙的响应式装饰器 @Provide/@Consume 实现了状态提升和跨组件通信,避免了深度嵌套对象属性观测的复杂性。主要优化空间在于多选操作的逻辑严谨性和类型安全。对于从 iOS 开发过渡到 ArkTS 来说,这是一个很实用的解决方案。

