HarmonyOS鸿蒙Next中循环渲染对象列表,页面无法观察到对象内部的属性变化

HarmonyOS鸿蒙Next中循环渲染对象列表,页面无法观察到对象内部的属性变化 SDK: 6.0.0(20)

操作步骤:进入页面,点击点赞图标,图标变化为’已点赞’或’未点赞’。

相关代码如下:

PostInfo类,使用**@Observed**修饰,isLike变化后,更新点赞图标

[@Observed](/user/Observed)
export class PostInfo {
  id: number;
  postText: string;
  rightCount: number;
  answeredCount: number;
  timeUsed: number;
  createTime: string;
  likeCount: number;
  nickname: string;
  avatarUrl: string;
  // 有初始值,才能建立观察通道
  @Track isLike: boolean = false;

  constructor(id: number, postText: string, rightCount: number, answeredCount: number,
    timeUsed: number, createTime: string, likeCount: number,
    nickname: string, avatarUrl: string, isLike: boolean) {
    this.id = id;
    this.postText = postText;
    this.rightCount = rightCount;
    this.answeredCount = answeredCount;
    this.timeUsed = timeUsed;
    this.createTime = createTime;
    this.likeCount = likeCount;
    this.nickname = nickname;
    this.avatarUrl = avatarUrl;
    this.isLike = isLike;
  }
}

API

// 分页获取打卡数据
export function getAllPost(param: PaginationParams): Promise<ApiResponse<PageData<PostInfo>>> {
  console.log('start getAllPost');
  return instance.get('/word/post/getAll', {params: param})
}

// 点赞
export function like(postId: number): Promise<ApiResponse<null>> {
  return instance.get('/word/like/create', {params: {postId: postId}})
}

// 取消点赞
export function cancelLike(postId: number): Promise<ApiResponse<null>> {
  return instance.get('/word/like/cancel', {params: {postId: postId}})
}

// 定义统一的API响应结构
export interface ApiResponse<T> {
  code: number | string;
  message?: string;
  data: T;
}

// 定义统一的分页返回类型
export interface PageData<T> {
  page: string;
  size: string;
  records: T[];
  total: number;
}

页面组件,获取分页数据,循环渲染。 postInfoList变化时,触发列表更新。 列表项中的PostInfo变量使用**@ObjectLink**修饰,但其中isLike变化时,未观察到点赞图标更新

@State postInfoList: PostInfo[] = []
@StorageProp('token') @Watch('onTokenChange') token: string = ''
@State isLoadingMore: boolean = true;
scroller: Scroller = new Scroller()
page:number = 1
total:number = 0

async getPostInfoPage() {
  const response = await getAllPost({page: this.page, size: 10})
  console.log(JSON.stringify(response));
  const data: PageData<PostInfo> = response.data
  this.total = data.total
  // push(...data.records)导致子元素的属性改变后无法被观察到
  // this.postInfoList.push(...data.records)
  this.postInfoList = [...this.postInfoList, ...data.records]
  this.page++
  this.isLoadingMore = false
}


 List({scroller: this.scroller}) {
    ForEach(this.postInfoList, (postInfo: PostInfo) => {
      ListItem() {
        PostItem({ post: postInfo })
      }
    }, (postInfo: PostInfo) => postInfo.id.toString())
  }


  @Component
struct PostItem {
  [@ObjectLink](/user/ObjectLink) post: PostInfo;

  build() {
    Column({ space: 10 })  
          
      Row() {
       
        Column() {
          Text(this.post.likeCount.toString())
            .fontSize(12)
            .fontWeight(FontWeight.Medium)
            .fontColor(this.post.isLike ? '#3ECBA1' : '#000000')
          Image(this.post.isLike ?
            $r('app.media.ic_post_like_selected') : $r('app.media.ic_post_like'))
            .width(26)
            .height(26)
            .onClick(async () => {
              if (this.post.isLike) {
                // 取消点赞
                console.log('取消点赞')
                let response = await cancelLike(this.post.id)
                if (response.code === 200) {
                  this.post.isLike = false
                  this.post.likeCount--
                  this.getUIContext().getPromptAction().showToast({message: '取消点赞成功'})
                }
              } else {
                // 点赞
                console.log('点赞')
                let response = await like(this.post.id)
                if (response.code === 200) {
                  this.post.isLike = true
                  this.post.likeCount++
                  this.getUIContext().getPromptAction().showToast({message: '点赞成功'})
                }
              }
            })
        }.width(50)
      }.width('100%')
      .alignItems(VerticalAlign.Bottom)
    }
    .padding(10)
    .width('90%')
    .margin({ top: 10 })
    .borderRadius(10)
    .shadow({ radius: 20 })
  }
}

更多关于HarmonyOS鸿蒙Next中循环渲染对象列表,页面无法观察到对象内部的属性变化的实战教程也可以访问 https://www.itying.com/category-93-b0.html

3 回复

楼主您好,您这边UI未更新原因是**@ObjectLink修饰的变量,没有通过new的方式实例化,可以看一下下文中的【解决方案】**,已经根据问题描述中的代码完成修改:

【背景知识】

[@Observed@ObjectLink装饰器](https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/arkts-observed-and-objectlink):该装饰器可以监测多层嵌套的情况,比如二维数组,或者数组项class中class属性变化,或者class的属性是class中内部class的属性变化。

ForEach循环渲染:在ForEach组件进行非首次渲染时,它会检查新生成的键值是否在上次渲染中已经存在。如果键值不存在,则会创建一个新的组件;如果键值存在,则不会创建新的组件,而是直接渲染该键值所对应的组件。详情参考:组件创建规则

【问题定位】

List组件内UI不刷新的问题可能有以下几种情况:

  1. @State无法观察到对象数组中对象属性的变化的这样也会导致UI无法刷新,需要使用@Observed@ObjectLink进行修饰。
  2. @ObjectLink修饰的变量,没有通过new的方式实例化。
  3. 根据知识背景中的组件创建规则,数据源修改后,如果监测方法正确,ForEach中键值对的值也会跟着修改,但是如果ForEach内keyGenerator(代表键值对中的键)没有更新也会导致无法刷新UI,ForEach参数介绍详见:ForEach参数介绍
  4. @ObjectLink修饰的变量含有嵌套类对象,组件改变嵌套对象的属性,无法刷新UI。因为@ObjectLink仅能观察其代理的属性,无法观察到代理属性的嵌套对象属性。参考:复杂嵌套对象属性更改失效

【解决方案】

楼主目前UI未触发更新的场景属于第二种现象:@ObjectLink修饰的变量,没有通过new的方式实例化。 建议通过new实例化后再处理,我这边根据您的代码进行修改,能够正常更新点赞,您可以看下:

[@Observed](/user/Observed)
export class PostInfo {
  id: number;
  postText: string;
  @Track likeCount: number;
  // 有初始值,才能建立观察通道
  @Track isLike: boolean = false;


  constructor(id: number, postText: string, isLike: boolean, likeCount: number) {
    this.id = id;
    this.postText = postText;
    this.isLike = isLike;
    this.likeCount = likeCount;
  }
}
// 在设置页面中使用
@Entry
@Component
struct shareTestComponent {
  [@State](/user/State) postInfoList: PostInfo[] = []
  [@State](/user/State) isLoadingMore: boolean = true;
  scroller: Scroller = new Scroller()
  page: number = 1
  total: number = 0

  async getPostInfoPage() {
    this.postInfoList = [new PostInfo(1122, '测试点赞', false, 0), new PostInfo(1123, '测试点赞2', false, 0)]
  }

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

  build() {
    Column() {
      List({ scroller: this.scroller }) {
        ForEach(this.postInfoList, (postInfo: PostInfo) => {
          ListItem() {
            PostItem({ post: postInfo })
          }
        }, (postInfo: PostInfo) => postInfo.id.toString())
      }
    }
  }
}

@Component
struct PostItem {
  [@ObjectLink](/user/ObjectLink) post: PostInfo;

  build() {
    Column({ space: 10 }) {

      Row() {
        Text(this.post.likeCount.toString())
          .fontSize(12)
          .fontWeight(FontWeight.Medium)
          .fontColor(this.post.isLike ? '#3ECBA1' : '#000000')
        Column() {
          Image(this.post.isLike ?
            $r('app.media.video_create_gif_preview3') : $r('app.media.video_create_gif_preview5'))
            .width(26)
            .height(26)
            .onClick(async () => {
              if (this.post.isLike) {
                this.post.isLike = false;
                this.post.likeCount--
                // 取消点赞
                this.getUIContext().getPromptAction().showToast({ message: '取消点赞成功' })
              } else {
                // 点赞
                console.log('点赞')

                this.post.isLike = true
                this.post.likeCount++
                this.getUIContext().getPromptAction().showToast({ message: '点赞成功' })

              }
            })
        }.width(50)
      }.width('100%')
      .alignItems(VerticalAlign.Bottom)
    }
    .padding(10)
    .width('90%')
    .margin({ top: 10 })
    .borderRadius(10)
    .shadow({ radius: 20 })
  }
}

其它UI不刷新的问题具体修改方式如下,也可以一并参考:

【分析结论】

  1. 建议将ForEach的最后一个参数缺省,系统会默认设置键值中的键。
  2. 使用@Observed@ObjectLink进行修饰。
  3. @ObjectLink修饰的变量通过new的方式实例化。
  4. @ObjectLink修饰的变量含有嵌套类对象,需要给子组件单独传递该嵌套类对象,子组件中new一个用@ObjectLink修饰的嵌套类对象来接收。

【修改建议】 场景一:@Observed结合@ObjectLink观察嵌套类对象属性变化。 可以参考如下实现方式:

[@Observed](/user/Observed)
class TestList {
  text: string;
  isFlag: boolean;

  constructor(text: string, isFlag: boolean) {
    this.text = text;
    this.isFlag = isFlag;
  }
}

@Entry
@Component
struct DisplayTest {
  [@State](/user/State) ListArray: Array<TestList> = [new TestList('第一条数据:', false), new TestList('第二条数据:', true)]

  build() {
    Column({ space: 20 }) {
      Text('测试:')
        .width('100%')
      Button('点击修改第一条数据的值!')
        .type(ButtonType.Capsule)
        .onClick(() => {
          this.ListArray[0].isFlag = !this.ListArray[0].isFlag
        })
        .width('50%')
      Button('点击新增加数据!')
        .type(ButtonType.Capsule)
        .onClick(() => {
          this.ListArray.push(new TestList('新增:', false))
        })
        .width('50%')
      Scroll() {
        Column() {
          ForEach(this.ListArray, (item: TestList) => {
            Test001({ test: item })
              .height(72)
          })
        }
      }
      .align(Alignment.TopStart)
      .layoutWeight(1)
    }
    .width('100%')
    .height('100%')
    .justifyContent(FlexAlign.Center)
  }
}

@Component
struct Test001 {
  [@ObjectLink](/user/ObjectLink) test: TestList

  build() {
    Row() {
      Text(this.test.text + this.test.isFlag)
    }
    .justifyContent(FlexAlign.Center)
    .height(100)
    .width('100%')
  }
}

场景二:@ObjectLink修饰的变量含有嵌套类对象,将该嵌套类对象单独传递给子组件。 可以参考如下实现方式:

@Entry
@Component
struct Index {
  [@State](/user/State) listArray: Person[] = []

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

  initData() {
    const p1 = new Person()
    p1.name = "p1"
    p1.id = "id1"

    const c1 = new Child()
    c1.name = "c1"
    c1.id = "cid1"

    const goods = new GoodsModel()
    goods.goodName = "大米"
    c1.goods = goods

    const c2 = new Child()
    c2.name = "c2"
    c2.id = "cid2"

    p1.children = [c1, c2]
    this.listArray = [p1]
  }

  build() {
    Column() {
      Button("更改数据")
        .fontSize(20)
        .margin({
          top: 50, bottom: 50
        })
        .onClick(() => {
          this.listArray.forEach((person: Person) => {
            person.children?.forEach((child: Child) => {
              if (child.id == "cid1") {
                if (!!child.goods) {
                  child.goods.goodName = "大米_002" // 修改嵌套类对象属性
                }
              }
            })
          })
        })
      this.contentList()

    }
    .justifyContent(FlexAlign.Start)
    .width("100%")
    .height("100%")
  }

  @Builder
  contentList() {
    List() {
      ForEach(this.listArray, (person: Person) => {
        ListItem() {
          List() {
            ForEach(person.children, (child: Child) => {
              ListItem() {
                ChildrenItemCard({
                  child: child,
                  goods: child.goods
                })
              }
            })
          }
        }
      }, (person: Person) => person.id)
    }
  }
}

[@Observed](/user/Observed)
export class Person {
  name?: string
  age: number = 0
  id: string = ""
  children?: Child[]
}

[@Observed](/user/Observed)
export class Child {
  name?: string
  age: number = 0
  id: string = ""
  goods?: GoodsModel
}

[@Observed](/user/Observed)
export class GoodsModel {
  goodName?: string
}

@Component
export struct ChildrenItemCard {
  [@ObjectLink](/user/ObjectLink) @Watch("updateModel") child: Child
  [@ObjectLink](/user/ObjectLink) goods: GoodsModel // 将嵌套类对象单独传递给子组件,不然无法观测到其属性变化

  updateModel() {
    console.debug("xx")
  }

  build() {
    Column() {
      Row() {
        Text("child.name:")
        Text(this.child.name ?? "")
      }
      .justifyContent(FlexAlign.Center)
      .width("90%")
      .height(30)

      Row() {
        Text("child.good.goodName:")
        Text(this.goods?.goodName ?? "")
      }
      .justifyContent(FlexAlign.Center)
      .width("90%")
      .height(30)
    }
  }
}

更多关于HarmonyOS鸿蒙Next中循环渲染对象列表,页面无法观察到对象内部的属性变化的实战系列教程也可以访问 https://www.itying.com/category-93-b0.html


在HarmonyOS Next中,使用@State@Observed装饰器管理对象列表时,若对象内部属性变化未触发UI更新,是因为装饰器仅监测对象引用变化。
需使用@Observed装饰对象类,并用@ObjectLink装饰组件内引用该对象的变量,或使用@Track装饰对象内需监听的属性。

例如:

[@Observed](/user/Observed) class Item {  
  [@Track](/user/Track) name: string = '';  
}  

组件中使用[@ObjectLink](/user/ObjectLink) item: Item

这样对象内部属性变更时,UI可同步更新。

这个问题是由于在 PostItem 组件中直接修改 @ObjectLink 装饰的 post 对象的属性时,状态管理机制未能正确触发UI更新。在HarmonyOS Next中,@ObjectLink 装饰的变量需要遵循特定的更新规则。

核心问题@ObjectLink 装饰的变量虽然可以观察到对象内部属性的变化,但直接修改其属性(如 this.post.isLike = false)可能不会触发UI重新渲染。

解决方案:需要确保对 @Observed 类实例的属性修改是通过 重新赋值整个对象使用可观察的更新方式 来实现。

以下是修改建议:

  1. PostInfo 类中添加一个更新方法
@Observed
export class PostInfo {
  // ... 其他属性

  updateLikeStatus(isLike: boolean): void {
    this.isLike = isLike;
    this.likeCount = isLike ? this.likeCount + 1 : this.likeCount - 1;
  }
}
  1. 修改 PostItem 组件中的点击事件处理
.onClick(async () => {
  const newLikeStatus = !this.post.isLike;
  
  if (newLikeStatus) {
    // 点赞
    let response = await like(this.post.id);
    if (response.code === 200) {
      this.post.updateLikeStatus(true);
    }
  } else {
    // 取消点赞
    let response = await cancelLike(this.post.id);
    if (response.code === 200) {
      this.post.updateLikeStatus(false);
    }
  }
})
  1. 或者,在父组件中维护状态更新: 如果上述方法仍不生效,可以考虑在父组件中维护状态更新:
// 在父组件中添加更新方法
updatePostLikeStatus(postId: number, isLike: boolean): void {
  const index = this.postInfoList.findIndex(item => item.id === postId);
  if (index !== -1) {
    // 创建新对象替换原对象
    const updatedPost = { ...this.postInfoList[index] };
    updatedPost.isLike = isLike;
    updatedPost.likeCount = isLike ? updatedPost.likeCount + 1 : updatedPost.likeCount - 1;
    
    // 替换数组中的对象
    this.postInfoList = [
      ...this.postInfoList.slice(0, index),
      updatedPost,
      ...this.postInfoList.slice(index + 1)
    ];
  }
}

然后在 PostItem 中通过回调调用父组件的方法。

关键点

  • @ObjectLink 装饰的变量需要确保对对象属性的修改能够被状态管理系统检测到
  • 对于复杂对象,推荐使用方法封装属性更新
  • 如果直接属性赋值不生效,考虑创建新对象替换原对象

这种模式确保了状态变化的可观察性,能够正确触发UI更新。

回到顶部