HarmonyOS鸿蒙Next中请教下数组里的数据处理问题

HarmonyOS鸿蒙Next中请教下数组里的数据处理问题

@ObservedV2
export class controlBtnModel {
  icon: ResourceStr = ''
  text: string = ''
  @Trace selected: boolean = false
  code: number = 0
}

@Entry
@ComponentV2
struct TestPage {
  private controlBtnsTitleList: string[] = ['自动', '轻柔', '中速', '急速']
  @Local controlBtns: Array<controlBtnModel> = []

  aboutToAppear(): void {
    this.getStatus()
  }

  getStatus() {
    const random = Math.floor(Math.random() * 4)
    this.controlBtns = this.handleControlBtns(this.controlBtnsTitleList, random)
  }

  handleControlBtns(controlBtnsTitleList: string[], index: number): controlBtnModel[] {
    const controlBtns: Array<controlBtnModel> = []
    controlBtnsTitleList.forEach(title => {
      const controlBtn: controlBtnModel = new controlBtnModel()
      switch (title) {
        case '自动':
          controlBtn.text = title
          controlBtn.code = 0
          controlBtn.icon = $r('app.media.icon_wind_auto')
          controlBtn.selected = index === controlBtn.code
          break;
        case '轻柔':
          controlBtn.text = title
          controlBtn.code = 1
          controlBtn.icon = $r("app.media.icon_wind_low")
          controlBtn.selected = index === controlBtn.code
          break;
        case '中速':
          controlBtn.text = title
          controlBtn.code = 2
          controlBtn.icon = $r("app.media.icon_wind_mid")
          controlBtn.selected = index === controlBtn.code
          break;
        case '急速':
          controlBtn.text = title
          controlBtn.code = 3
          controlBtn.icon = $r("app.media.icon_wind_high")
          controlBtn.selected = index === controlBtn.code
          break;
      }
      controlBtns.push(controlBtn)
    })
    return controlBtns
  }

  @Builder
  controlBtnBuilder(btn: controlBtnModel, index: number) {
    Column({ space: 5 }) {
      Image(btn.icon)
        .width(25)
        .height(25)
      Text(btn.text)
    }
    .border({
      width: { left: index != 0 ? 1 : 0 },
      color: { left: index != 0 ? '#D4D4DC' : Color.Transparent }
    })
    .backgroundColor(btn.selected ? 'rgba(121,121,121,0.2)' : Color.White)
    .layoutWeight(1)
    .alignItems(HorizontalAlign.Center)
    .justifyContent(FlexAlign.Center)
    .height('100%')
    .onClick(() => {
      if (btn.selected) {
        return
      }

      this.controlBtns.forEach(item => {
        item.selected = false
      })
      btn.selected = true
    })
  }

  build() {
    Column({ space: 20 }) {
      Row() {
        ForEach(this.controlBtns, (btn: controlBtnModel, index: number) => {
          this.controlBtnBuilder(btn, index)
        }, (item: controlBtnModel) => JSON.stringify(item))
      }
      .clip(true)
      .margin({ top: 55 })
      .borderWidth(1)
      .borderColor('#D4D4DC')
      .borderRadius(10)
      .height(70)
      .width('100%')

      Button('重新从服务器拿数据')
        .onClick(() => {
          this.getStatus()
        })
    }
    .padding({ left: 16, right: 16 })
    .height('100%')
    .width('100%')
  }
}

代码如上,不点击下方按钮只点击列表里的按钮时好用,不点击列表里的按钮只点击下方按钮时也好用,但是两种按钮都点击后页面UI就乱了,但是通过log打印数据是对的,求教。。。


更多关于HarmonyOS鸿蒙Next中请教下数组里的数据处理问题的实战教程也可以访问 https://www.itying.com/category-93-b0.html

7 回复

这是一个在使用ArkTS状态管理V2时可能遇到的典型问题。问题的核心在于 数据状态与UI渲染的同步机制

问题分析与解决方案

核心原因

在您的代码中,controlBtnModel 类被 @ObservedV2 装饰,其 selected 属性被 @Trace 装饰,这确保了该属性的变化可以被观测到。然而,问题出在 您直接修改了数组中原有对象的属性,但并未改变数组本身的引用

当您点击下方的“重新从服务器拿数据”按钮时,getStatus() 方法会创建一个全新的 controlBtns 数组(通过 handleControlBtns 返回新数组),这会触发UI的整体刷新。

但当您点击列表中的按钮时,您是在原地修改当前数组内对象的属性:

this.controlBtns.forEach(item => {
  item.selected = false
})
btn.selected = true

虽然 @Trace 装饰器能观察到 selected 属性的变化并触发局部的、细粒度的UI更新,但这种更新可能和之前通过整体替换数组(this.controlBtns = ...)所建立的UI渲染树产生某种冲突或状态不一致,导致UI显示混乱。

解决方案

为确保数据状态变化能稳定、可靠地驱动UI更新,推荐以下两种方法:

方法一:始终通过替换整个数组来更新状态(推荐) 这是最符合ArkUI响应式编程理念的做法。每次更新都创建一个全新的数组,让框架能够清晰地识别出数据变化并完整地刷新UI。

修改您的 onClick 事件处理函数:

.onClick(() => {
  if (btn.selected) {
    return
  }
  
  // 创建当前数组的一个深度副本
  const newControlBtns = this.controlBtns.map(item => {
    // 创建新的 controlBtnModel 对象,而不是修改原对象
    const newItem = new controlBtnModel();
    newItem.icon = item.icon;
    newItem.text = item.text;
    newItem.code = item.code;
    // 设置选中状态:只有当前按钮被选中,其他都是false
    newItem.selected = (item.code === btn.code);
    return newItem;
  });
  
  // 替换整个数组,触发UI整体刷新
  this.controlBtns = newControlBtns;
})

方法二:确保类的所有关键属性都被正确装饰 如果 controlBtnModel 中还有其他应在变化时触发UI更新的属性,确保它们也被 @Trace 装饰。但即便如此,方法一(替换整个数组)通常是更安全、更不易出错的选择。

总结

您遇到的UI混乱问题,其根本原因是混合使用了两种状态更新策略:一种是细粒度的属性更新(由@Trace驱动),另一种是粗粒度的数组整体替换。这有时会导致框架的渲染树内部状态不一致。

最佳实践是选择其中一种策略并始终坚持。对于数组渲染的场景,方法一(整体替换数组) 因其简单性和可靠性而被广泛推荐。每次更新都产生一个新的数据源,可以确保UI与数据状态完全同步。

按照上述方法修改代码后,无论是点击列表按钮还是点击下方刷新按钮,UI都应该能正确响应并显示了。

更多关于HarmonyOS鸿蒙Next中请教下数组里的数据处理问题的实战系列教程也可以访问 https://www.itying.com/category-93-b0.html


感谢回答 我也发现数据其实是对的,但是页面渲染混乱。就是说改变数组里某个元素的属性和整体替换数组两种方式同时使用改变数组的时候会导致渲染出现问题,解决办法是只用其中一种方式来改变数组,也就是只通过整体替换数组或者只改变数组里元素的属性的方法。我的困惑是为什么会出现这种框架内渲染状态不一致的情况。

1.对代码进行优化了楼主可以参考一下,第一次进入页面时:调用 handleControlBtns 初始化按钮数组,后续点击刷新按钮时:只更新 selected,不再 new 对象,ForEach key:使用 code 保证稳定;

2.参考代码:

@ObservedV2
export class controlBtnModel {
  icon: ResourceStr = ''
  text: string = ''
  @Trace selected: boolean = false
  code: number = 0
}

@Entry
@ComponentV2
struct FocusTestPage {
  @Local controlBtns: Array<controlBtnModel> = []
  private controlBtnsTitleList: string[] = ['自动', '轻柔', '中速', '急速']

  aboutToAppear(): void {
    // 初始化时只调用一次 handleControlBtns
    const random = Math.floor(Math.random() * 4)
    this.controlBtns = this.handleControlBtns(this.controlBtnsTitleList, random)
  }

  // 只在初始化时用,生成按钮对象数组
  handleControlBtns(controlBtnsTitleList: string[], index: number): controlBtnModel[] {
    const controlBtns: Array<controlBtnModel> = []
    controlBtnsTitleList.forEach(title => {
      const controlBtn: controlBtnModel = new controlBtnModel()
      switch (title) {
        case '自动':
          controlBtn.text = title
          controlBtn.code = 0
          controlBtn.icon = $r('app.media.foreground')
          break;
        case '轻柔':
          controlBtn.text = title
          controlBtn.code = 1
          controlBtn.icon = $r("app.media.layered_image")
          break;
        case '中速':
          controlBtn.text = title
          controlBtn.code = 2
          controlBtn.icon = $r("app.media.app_icon")
          break;
        case '急速':
          controlBtn.text = title
          controlBtn.code = 3
          controlBtn.icon = $r("app.media.background")
          break;
      }
      controlBtn.selected = index === controlBtn.code
      controlBtns.push(controlBtn)
    })
    return controlBtns
  }

  // 后续刷新时只更新 selected,不再 new 对象
  getStatus() {
    const random = Math.floor(Math.random() * 4)
    this.controlBtns.forEach(btn => {
      btn.selected = (btn.code === random)
    })
  }

  @Builder
  controlBtnBuilder(btn: controlBtnModel, index: number) {
    Column({ space: 5 }) {
      Image(btn.icon)
        .width(25)
        .height(25)
      Text(btn.text)
    }
    .border({
      width: { left: index != 0 ? 1 : 0 },
      color: { left: index != 0 ? '#D4D4DC' : Color.Transparent }
    })
    .backgroundColor(btn.selected ? 'rgba(121,121,121,0.2)' : Color.White)
    .layoutWeight(1)
    .alignItems(HorizontalAlign.Center)
    .justifyContent(FlexAlign.Center)
    .height('100%')
    .onClick(() => {
      if (btn.selected) {
        return
      }
      this.controlBtns.forEach(item => {
        item.selected = false
      })
      btn.selected = true
    })
  }

  build() {
    Column({ space: 20 }) {
      Row() {
        ForEach(this.controlBtns, (btn: controlBtnModel, index: number) => {
          this.controlBtnBuilder(btn, index)
        }, (item: controlBtnModel) => item.code.toString()) // key 用 code 保证稳定
      }
      .clip(true)
      .margin({ top: 55 })
      .borderWidth(1)
      .borderColor('#D4D4DC')
      .borderRadius(10)
      .height(70)
      .width('100%')

      Button('重新从服务器拿数据')
        .onClick(() => {
          this.getStatus()
        })
    }
    .padding({ left: 16, right: 16 })
    .height('100%')
    .width('100%')
  }
}

JSON.stringify(item)作为ForEach的key生成函数存在隐患:当对象属性顺序变化时可能导致相同的JSON字符串,建议改为唯一标识符

ForEach(this.controlBtns, 
  (btn: controlBtnModel) => btn.code.toString(), // 使用code作为唯一键
  (item) => this.controlBtnBuilder(item)
)

问题出在数组对象的响应式更新机制上。@Local装饰器仅支持组件内部修改触发更新,但直接修改对象属性不会触发UI刷新;直接修改数组元素属性时,ArkUI框架无法感知到对象级别的变化;使用JSON.stringify(item)可能导致键值重复或不稳定

解决方案

修改状态管理装饰器:将@Local改为@State确保数组变化可观测

[@State](/user/State) controlBtns: Array<controlBtnModel> = []

优化对象更新方式:创建新对象触发响应式更新

// 点击事件修改为

onClick(() => {

  if (btn.selected) return;

  

  this.controlBtns = this.controlBtns.map(item => {

    const newItem = new controlBtnModel();

    Object.assign(newItem, item);

    newItem.selected = (item.code === btn.code);

    return newItem;

  });

})

调整ForEach键值生成策略:使用code作为唯一标识

ForEach(this.controlBtns, (btn: controlBtnModel, index: number) => {

  this.controlBtnBuilder(btn, index)

}, (item: controlBtnModel) => item.code.toString())

鸿蒙Next中数组数据处理主要使用ArkTS的集合操作API。通过Array类提供的方法可实现过滤、映射、排序等操作。例如:filter()进行条件筛选,map()进行元素转换,sort()进行排序。还支持forEach()遍历和reduce()聚合计算。这些方法均基于ArkTS语言特性,符合鸿蒙生态开发规范。

在HarmonyOS Next中,你遇到的问题是由于@Local装饰器与@ObservedV2类的交互导致的UI更新不一致。当点击列表按钮时,直接修改了数组元素的selected属性,但@Local装饰的数组可能不会触发完整的UI刷新,尤其是在与ForEach结合使用时。

建议改用@State装饰器来声明controlBtns数组,因为@State能更好地管理数组内部对象属性的变化并同步更新UI:

@State controlBtns: Array<controlBtnModel> = []

这样修改后,无论是通过下方按钮重新生成数组,还是直接点击列表项修改选中状态,UI都能正确响应数据变化。@State会自动检测数组内对象属性的变更并触发构建函数,确保视图与数据一致。

回到顶部