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

2 回复

在HarmonyOS Next中,处理多层嵌套数据的多选和单选,可以使用ArkUI的@State@Link装饰器管理状态。通过递归组件或ForEach循环渲染嵌套结构,结合CheckboxGroup或自定义选择逻辑实现。多选时维护选中项ID集合;单选时使用变量记录当前选中项。数据变更通过状态管理自动更新UI。

更多关于HarmonyOS 鸿蒙Next中实现多层嵌套数据的多选单选处理的实战系列教程也可以访问 https://www.itying.com/category-93-b0.html


你的思路是可行的,使用 @Provide@Consume 在多层嵌套组件间共享一个状态管理 Map 来记录选中项,这确实绕过了 @Observed@ObjectLink 在处理复杂嵌套对象时的某些限制。

针对你的实现,有几个关键点可以优化和明确:

  1. 状态管理的核心:你使用 Map<string, string | string[]> 作为核心状态容器是合理的。Key 通常是父级(如 B 的 id),Value 是子级选中项的标识(单选 string,多选 string[])。这实现了状态与数据模型的解耦。

  2. @Provide / @Consume 的适用性:在这个场景下,它们比 @Observed / @ObjectLink 更直接。后者要求对每个嵌套层级的类使用 @Observed 装饰,并在子组件中 @ObjectLink 该类的实例,才能实现深度的、可观测的属性变更。对于你已有的数据模型(A, B, C),改造起来可能更繁琐。@Provide / @Consume 提供了跨层级的、类似“全局上下文”的状态共享,更符合你“集中管理ID”的思路。

  3. 代码优化点

    • 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)
      
    • 类型安全:在 CMItemCSItem 中,对 this.selects.get() 的结果进行 as string[]as string 的类型断言存在风险。建议在 Map 的 Value 类型定义上更精确,或者在使用前进行类型检查。
  4. 自动滑动到节点:你示例中通过 ScrollerscrollToIndex 实现。这需要你预先计算好每个 BItem(或目标项)在扁平化列表中的索引。你的示例通过硬编码索引演示了原理,在实际应用中,需要根据数据结构和选中项动态计算这个索引。

总结:你当前的架构是有效的。它利用鸿蒙的响应式装饰器 @Provide/@Consume 实现了状态提升和跨组件通信,避免了深度嵌套对象属性观测的复杂性。主要优化空间在于多选操作的逻辑严谨性和类型安全。对于从 iOS 开发过渡到 ArkTS 来说,这是一个很实用的解决方案。

回到顶部