从静态到动态:鸿蒙 ArkTS 列表组件与状态装饰器实战

从静态到动态:鸿蒙 ArkTS 列表组件与状态装饰器实战

前言

在上一篇入门内容中,我们已经掌握了 ArkTS 的基础页面结构、输入框与按钮等交互组件,实现了简单的用户输入交互。而这周,我完成了列表组件List与基础状态装饰器的学习,终于把之前的静态页面,升级成了可以动态渲染、支持数据更新的动态列表 ------ 这也是我们开发实际业务页面的核心一步。

很多新手在入门到这一步的时候,很容易搞混各种带@的装饰器,也会踩列表渲染的各种坑,所以这篇文章我把自己的学习笔记、踩过的坑都整理了出来,既能帮自己复盘加深理解,也希望能帮同样在入门鸿蒙开发的开发者少走弯路。

开发环境:DevEco Studio、API 10+

前置知识:已掌握 ArkTS 基础页面结构、@State基础用法、输入框与按钮交互


一、先搞懂:基础状态装饰器到底是什么

在学列表之前,我们得先把之前刚学的状态装饰器 理清楚 ------ 这些带@的语法,是 ArkTS 实现响应式的核心,也是我们能做到 "数据改了 UI 自动更" 的关键。

入门阶段最常用的两个基础装饰器,其实很好区分:

装饰器 作用范围 核心特点 适用场景
@State 组件内部私有 组件自己的状态,修改后自动触发 UI 刷新 管理组件自己的数据,比如列表数据、输入框内容
@Prop 父子组件间 接收父组件传递的参数,单向绑定,子组件修改不影响父组件 自定义子组件,接收父组件传过来的展示数据

简单来说:

  • @State就是 "我自己的东西,我改了,我自己的 UI 跟着变"
  • @Prop就是 "我爸给我的东西,我能看能用,但是我改了没用,我爸的东西不会变"

这就是 ArkTS 最基础的单向数据流:父组件管数据,子组件只管展示,子组件要改数据,得通知父组件来改,不能自己偷偷改 ------ 这也是新手最容易踩的第一个坑。

基础用法示例

typescript 复制代码
// 子组件:展示单个用户信息
@Component
struct UserCard {
  // 用@Prop接收父组件传过来的用户数据
  @Prop userName: string;
  @Prop age: number;

  build() {
    Column() {
      Text("姓名:" + this.userName)
      Text("年龄:" + this.age)
    }
    .padding(15)
    .backgroundColor('#F5F5F5')
    .borderRadius(8)
  }
}

// 父组件
@Entry
@Component
struct ParentPage {
  // 用@State管理自己的用户数据
  @State user: {name: string, age: number} = {name: "张三", age: 20};

  build() {
    Column() {
      // 把数据传给子组件
      UserCard({ userName: this.user.name, age: this.user.age })

      Button("修改年龄")
        .onClick(() => {
          // 父组件自己改@State的数据,UI自动刷新
          this.user.age++;
        })
    }
    .width('100%')
    .justifyContent(FlexAlign.Center)
    .padding(20)
  }
}

二、列表组件 List:动态列表的核心

搞定了状态装饰器,我们再来学列表组件List------ 这是我们开发商品列表、通讯录、待办清单这类页面的核心,它可以承载大量的列表项,还自带滑动能力,不用我们自己写。

1. List 的基础结构

List是容器组件,里面的每一项都是ListItem,基础结构很简单:

typescript 复制代码
List() {
  ListItem() {
    Text("列表项1")
  }
  ListItem() {
    Text("列表项2")
  }
}

但如果我们只有静态的几个项,那根本没必要用 List,List 的真正用处,是动态渲染数组数据 ,这就要用到循环渲染ForEach了。

2. 循环渲染 ForEach:把数组变成列表

ForEach是 ArkTS 提供的循环渲染 API,它可以遍历我们的数组,把每个数组元素变成对应的 UI 组件,这是动态列表的核心。

它有三个参数,我们必须记清楚:

  1. 要遍历的数组
  2. 遍历函数:把每个数组元素转成 UI 组件
  3. key 生成函数:给每个项生成唯一的 key,**这个千万不能忘!
typescript 复制代码
// 模拟的列表数据
@State listData: string[] = ["第一项", "第二项", "第三项"];

build() {
  List() {
    // 遍历数组,生成列表项
    ForEach(
      this.listData, 
      (item: string) => {
        ListItem() {
          Text(item)
            .padding(15)
        }
      },
      // 生成唯一key,用数组的索引或者数据的唯一id
      (item: string) => item
    )
  }
  .width('100%')
}

这里一定要强调:key 必须是唯一的,不能重复

很多新手刚学的时候,会忽略这个 key,结果就是当列表数据更新(比如删除、新增项)的时候,列表会出现渲染错乱,比如删除了第一项,第二项的状态跑到第三项去了 ------ 这就是因为没有 key,系统不知道哪个项对应哪个数据,只能按顺序复用,就乱掉了。

3. List 的常用配置

List 有几个非常常用的属性,我们做列表的时候基本都会用到:

  • space:列表项之间的间距
  • divider:列表的分割线,用来区分不同的项
  • flexGrow:让 List 占满剩余空间,这样内容超出的时候才会自动滑动
typescript 复制代码
List() {
  // ...列表项
}
.width('90%')
.flexGrow(1) // 占满剩余空间,实现滑动
.space(10) // 列表项之间的间距
.divider({ // 分割线配置
  strokeWidth: 1, // 分割线宽度
  color: '#EEEEEE', // 分割线颜色
  startMargin: 15, // 分割线左边距
  endMargin: 15 // 分割线右边距
})

三、综合实战:做一个动态待办列表

讲了这么多理论,我们来把所有知识点串起来,做一个完整的动态待办列表,这个小项目刚好能覆盖我们这周学的所有内容:

  • @State管理列表数据
  • @Prop实现父子组件传值
  • List+ForEach实现动态列表
  • 实现待办的新增、删除功能

完整可运行代码

typescript 复制代码
import { Column, List, ListItem, ForEach, Row, Text, TextInput, Button, 
         Color, FlexAlign, TextDecoration } from '@kit.ArkUI';

// 定义待办项的类型
interface Todo {
  id: number;
  content: string;
  done: boolean;
}

// 子组件:单个待办项
@Component
struct TodoItem {
  // 用@Prop接收父组件传过来的待办数据
  @Prop todo: Todo;
  // 接收父组件传过来的删除回调,用来通知父组件删除数据
  private onDelete: () => void;

  build() {
    ListItem() {
      Row() {
        Text(this.todo.content)
          .fontSize(16)
          // 已完成的待办,文字变灰+删除线
          .fontColor(this.todo.done ? '#999999' : '#333333')
          .decoration(this.todo.done ? TextDecoration.LineThrough : TextDecoration.None)

        // 占位组件,把删除按钮挤到最右边
        Blank()

        Button("删除")
          .width(60)
          .height(30)
          .fontSize(12)
          .backgroundColor(Color.Red)
          .onClick(() => {
            // 点击删除,调用父组件的回调,通知父组件更新数据
            this.onDelete();
          })
      }
      .width('100%')
      .padding(15)
      .backgroundColor(Color.White)
      .borderRadius(8)
    }
  }
}

// 入口页面
@Entry
@Component
struct TodoListPage {
  // 用@State管理整个待办列表的数据,父组件的私有状态
  @State todoList: Todo[] = [
    { id: 1, content: '学习ArkTS基础语法', done: true },
    { id: 2, content: '掌握List列表组件', done: false },
    { id: 3, content: '搞懂状态装饰器区别', done: false },
  ];
  // 输入框的内容
  @State inputValue: string = "";

  build() {
    Column() {
      // 页面标题
      Text("我的待办列表")
        .fontSize(24)
        .fontWeight(FontWeight.Bold)
        .margin({ top: 20, bottom: 20 })

      // 新增待办的输入栏
      Row() {
        TextInput({ placeholder: '请输入待办内容', text: this.inputValue })
          .flexGrow(1)
          .height(45)
          .backgroundColor('#F5F5F5')
          .borderRadius(8)
          .padding({ left: 15 })
          .onChange((value) => {
            this.inputValue = value;
          })

        Button("添加")
          .width(70)
          .height(45)
          .margin({ left: 10 })
          .backgroundColor(Color.Blue)
          .onClick(() => {
            if (this.inputValue.trim() === "") return;
            // 新增待办,修改@State数据,UI自动刷新
            this.todoList.push({
              id: Date.now(), // 用时间戳做唯一id
              content: this.inputValue,
              done: false
            });
            // 清空输入框
            this.inputValue = "";
          })
      }
      .width('90%')
      .margin({ bottom: 15 })

      // 待办列表
      List() {
        // 循环渲染所有待办项
        ForEach(
          this.todoList, 
          (todo: Todo) => {
            TodoItem({
              todo: todo,
              // 给子组件传删除回调,父组件处理删除逻辑
              onDelete: () => {
                this.todoList = this.todoList.filter(item => item.id !== todo.id);
              }
            })
          },
          // 唯一key,用待办的id,保证不重复
          (todo: Todo) => todo.id.toString()
        )
      }
      .width('90%')
      .flexGrow(1) // 让列表占满剩余空间,实现滑动
      .space(10) // 列表项间距
      .divider({ // 列表分割线
        strokeWidth: 1,
        color: '#EEEEEE',
        startMargin: 15,
        endMargin: 15
      })
    }
    .width('100%')
    .height('100%')
    .backgroundColor('#F5F5F5')
    .padding({ bottom: 20 })
  }
}

效果说明

把这段代码复制到你的 DevEco Studio 里,直接就能运行,你可以得到一个完整的待办列表:

  • 可以输入新的待办,点击添加,列表会自动新增项
  • 可以点击删除,删掉对应的待办
  • 列表内容多了之后,会自动支持滑动
  • 已完成的待办会有灰色删除线的样式

四、学习踩坑:新手最容易犯的 5 个错误

这周学习的时候,我踩了不少坑,整理出来给大家避避:

  1. ForEach 忘记加 key:刚开始写列表的时候,我没加 key,结果删除待办的时候,剩下的项的状态全乱了,查了半天才发现是 key 的问题。
  2. 子组件直接改 @Prop :刚开始不懂单向数据流,想在子组件里直接改this.todo.done,结果发现父组件的数据根本没变化,后来才知道要通知父组件来改。
  3. List 没加高度限制,导致不能滑动 :刚开始给 List 写了wrapContent的高度,结果内容再多也不会滑动,后来才知道要给 List 设置固定高度或者flexGrow,让它有自己的滚动区域。
  4. 以为修改数组属性不会触发更新 :刚开始我以为要给todoList重新赋值整个新数组才会刷新,后来发现直接push、修改对象属性,@State都能检测到,自动刷新 UI。
  5. key 用了数组索引:刚开始图省事,用数组的索引当 key,结果删除项的时候,索引变了,还是会出现渲染错乱,后来改成用数据自己的唯一 id 就好了。

五、学习总结

这周从 "写静态页面" 进入了 "写动态应用" 的阶段,核心掌握了这些内容:

  1. 两个基础状态装饰器的区别:@State管组件内部状态,@Prop管父子传值
  2. 单向数据流的核心逻辑:父组件管数据,子组件只管展示,改数据要通知父组件
  3. 列表组件List的基础用法,以及ForEach循环渲染的正确姿势,尤其是 key 的重要性
  4. 把所有知识点串起来,开发一个完整的动态列表应用

学完这些,其实我们已经能开发大部分的基础业务页面了,比如商品列表、用户列表、待办清单这些,都是基于这些知识点。

相关推荐
maaath2 小时前
【无标题】Flutter for OpenHarmony 的文具手账应用开发实践
flutter·华为·harmonyos
李李李勃谦2 小时前
鸿蒙PC打造电子书阅读器:支持 EPUB/PDF、书签同步、笔记管理
笔记·华为·pdf·harmonyos
枫叶丹43 小时前
【HarmonyOS 6.0】Core File Kit:端云文件版本管理能力解析与实践
开发语言·华为·harmonyos
李李李勃谦3 小时前
鸿蒙PC数据查看器:大数据量快速加载、筛选与可视化图表
华为·harmonyos
枫叶丹43 小时前
【HarmonyOS 6.0】CANN Kit 新增支持获取 AI 模型 Dump 维测数据功能详解
开发语言·人工智能·华为·信息可视化·harmonyos
key_3_feng3 小时前
鸿蒙6.0父子组件通信深度解析
华为·harmonyos
李李李勃谦15 小时前
鸿蒙PC密码管理器实战:本地加密存储与自动填充完整实现
华为·harmonyos
Swift社区16 小时前
鸿蒙 App 架构中的“领域拆分”
华为·架构·harmonyos
maaath19 小时前
【maaath】Flutter for OpenHarmony 手表配饰应用实战开发
flutter·华为·harmonyos