鸿蒙 ArkTS 实战进阶:组件复用三剑客与状态管理一篇通

前言

在上一篇入门文章里,我们搞定了 ArkTS 的基础页面结构、Text/TextInput/Button 这些基础组件,实现了最简单的用户交互 —— 这时候我们已经能写出 “能跑” 的页面了,但离 “写得好、写得规范、写得高效” 还有一段距离。

这周我把 ArkTS 开发的核心进阶知识点全部啃完了:从动态列表渲染,到 @State/@Prop/@Link 这套状态管理体系,再到 @Styles/@Extend/@Builder 这三个组件复用神器,还有 Swiper 轮播、Badge 角标这些高频实用组件,以及 Flex/Stack 布局进阶。

从 “把页面写出来” 到 “把页面写得优雅、不写重复代码”,这一周的学习可以说是 ArkTS 入门的第一个分水岭。这篇文章我把自己的学习笔记、踩过的坑全部整理出来,既是自己的复盘加深,也希望能帮同样在入门的开发者少走弯路。

开发环境:DevEco Studio、API 10+

前置知识:已掌握 ArkTS 基础页面结构、基础组件与简单交互


一、动态列表:从写死的页面到数据驱动的列表

在上一篇里,我们的页面内容都是写死的,而实际开发里 90% 的页面都是列表:商品列表、消息列表、待办列表…… 这就是我们要学的第一个核心:List组件 + ForEach循环渲染。

1. List 和 Column 的本质区别

很多新手一开始会问:我用 Column 也能堆出列表,为什么要用 List?

答案很简单:List 自带组件复用和滚动性能优化

  • Column 会把所有子组件一次性全部渲染,超过 10 个项之后就会开始卡顿,而且没有自带滚动
  • List 只会渲染当前屏幕可见的项,滚动时自动复用组件,就算 1000 个项也能流畅滑动,自带滚动能力

2. ForEach 循环渲染:新手的第一个天坑

ForEach是 ArkTS 提供的响应式循环 API,它的作用就是把我们的数组数据,自动变成对应的 UI 组件,数据变了列表自动更新。它的语法很简单,但有一个 90% 的新手都会踩的坑:key 的设置

// 错误写法:用数组索引当key,删除/新增项时会渲染错乱
ForEach(this.todoList, (item, index) => {
  ListItem() {
    Text(item.content)
  }
}, (item, index) => index)

// 正确写法:用数据本身的唯一标识当key
ForEach(this.todoList, (item) => {
  ListItem() {
    Text(item.content)
  }
}, (item) => item.id.toString())

为什么不能用索引当 key?

因为 key 是框架识别 “这个组件是谁” 的唯一标记,如果你删除了第一个项,原来第二个项的索引就变成了 0,框架就会误以为 “这个组件没变,只是内容变了”,直接复用原来的组件,就会出现状态错乱、输入框内容错位的问题。

3. List 常用配置

List() {
  // ...列表项
}
.width('90%')
.flexGrow(1) // 核心:让List占满剩余空间,内容超出自动滚动
.space(10) // 列表项之间的间距
.divider({ // 列表分割线
  strokeWidth: 1,
  color: '#EEEEEE',
  startMargin: 15,
  endMargin: 15
})

这里再提一个新手坑:如果不给 List 设置固定高度或者flexGrow(1),它的高度会跟着内容自适应,内容再多也不会滚动 —— 我当时卡了一下午才发现这个问题。


二、状态管理:彻底搞懂三个带 @的核心装饰器

搞懂列表之后,最核心的就是 ArkTS 的状态管理体系 —— 这也是声明式 UI 的灵魂:数据变了,UI 自动刷新,不用我们手动改页面。

这周我学了三个最核心的状态装饰器,我用最通俗的方式给你讲明白:

装饰器 作用 数据流 初始化要求 适用场景
@State 组件内部私有状态 组件内双向:改数据→UI 更新,UI 更新→数据变 必须本地初始化赋值 组件自己的数据:列表数据、输入框内容
@Prop 父子组件单向传值 父→子单向:父组件数据变,子组件自动更;子组件改数据不影响父 可本地初始化,也可接收父组件传值 子组件接收父组件的展示数据
@Link 父子组件双向同步 父子双向:父变子也变,子变父也变 必须从父组件接收状态引用,不能本地初始化 表单输入、滑块开关,需要双向联动的场景

用生活化的比喻一下就懂:

  • @State:你自己的笔记本,你随便写随便改,只有你自己能用
  • @Prop:你朋友把他的笔记复印了一份给你,你在复印件上改,朋友的原件不会变
  • @Link:你和朋友共用一个在线文档,你改了他那边也变,他改了你这边也变

基础示例:父子组件传值

// 子组件:待办项
@Component
struct TodoItem {
  // 单向接收父组件传的内容
  @Prop content: string;
  // 双向绑定完成状态:子组件点勾选,父组件的状态也同步变
  @Link isDone: boolean;

  build() {
    ListItem() {
      Row() {
        Text(this.content)
          .decoration(this.isDone ? TextDecoration.LineThrough : TextDecoration.None)
        Checkbox({ name: 'todo' })
          .select(this.isDone)
          .onChange((value) => {
            // 子组件改@Link,父组件的状态自动同步
            this.isDone = value;
          })
      }
    }
  }
}

// 父组件
@Entry
@Component
struct TodoList {
  @State todoList: {id: number, content: string, done: boolean}[] = [
    {id: 1, content: '学List组件', done: true},
    {id: 2, content: '搞懂状态管理', done: false}
  ];

  build() {
    List() {
      ForEach(this.todoList, (item) => {
        TodoItem({
          content: item.content,
          // 用$符号传引用给@Link
          isDone: $item.done
        })
      }, item => item.id.toString())
    }
  }
}

这里注意:给@Link传值的时候,要加$符号,代表传的是状态的引用,而不是值本身 —— 这也是新手容易忘的点。


三、组件复用三剑客:告别重复代码,写优雅的 ArkTS

这是我这周收获最大的部分:之前写页面,同样的按钮样式、同样的文本样式,每个组件都要写一遍重复的代码,而这三个装饰器,就是专门解决代码复用的问题,让我们的代码干净又好维护。

1. @Styles:定义通用样式集合

@Styles专门用来封装通用的、所有组件都能用的样式属性,比如宽高、边距、背景色这些,解决重复写样式的问题。

// 全局@Styles:不用加this,所有组件都能用
@Styles function commonCard() {
  .width('90%')
  .padding(15)
  .backgroundColor(Color.White)
  .borderRadius(8)
  .shadow({ radius: 5, color: '#1A000000' })
}

// 使用:直接调用,一行搞定所有通用样式
Column() {
  Text("卡片内容")
}
.commonCard() // 直接复用样式

注意:@Styles只能写通用属性,不能写组件专属属性,比如 Text 的fontSize就不能写在里面。

2. @Extend:扩展原生组件

@Extend专门用来扩展系统原生组件,比如给 Text、Button、Image 批量加自定义样式、事件,生成 “增强版” 的原生组件。

// 扩展Button:定义一个全局的主按钮样式
@Extend(Button) function mainButton() {
  .width('90%')
  .height(50)
  .backgroundColor(Color.Blue)
  .borderRadius(25)
  .fontSize(16)
}

// 使用:所有主按钮直接用,不用重复写样式
Button("确认提交")
  .mainButton()

Button("下一步")
  .mainButton()
  .margin({ top: 10 })

@Styles的区别:@Extend是针对某个特定组件的扩展,可以写这个组件的专属属性,还能绑定事件;@Styles是通用的,所有组件都能用。

3. @Builder:封装可复用的 UI 片段

@Builder是三个里面功能最强的,它可以封装完整的 UI 结构,包括多个子组件、布局、逻辑,是最灵活的复用方式。

// 封装一个空状态的UI片段
@Builder function EmptyState(icon: Resource, tip: string) {
  Column() {
    Image(icon)
      .width(80)
      .margin({ bottom: 10 })
    Text(tip)
      .fontSize(14)
      .fontColor('#999999')
  }
  .width('100%')
  .height('100%')
  .justifyContent(FlexAlign.Center)
}

// 使用:列表为空的时候直接调用
if (this.todoList.length === 0) {
  this.EmptyState($r('app.media.icon_empty'), "暂无待办事项")
}

这三个装饰器用熟了之后,你的代码里再也不会有大段重复的样式和 UI 片段,维护起来太舒服了。


四、高频实用组件与布局进阶

1. Swiper 轮播组件

Swiper 是做首页轮播图、引导页、Tab 切换的核心组件,自带触摸滑动、循环、自动播放所有功能,不用自己手写:

Swiper() {
  Image($r('app.media.banner1'))
    .objectFit(ImageFit.Cover)
  Image($r('app.media.banner2'))
    .objectFit(ImageFit.Cover)
  Image($r('app.media.banner3'))
    .objectFit(ImageFit.Cover)
}
.width('100%')
.height(180)
.loop(true) // 开启无限循环
.autoPlay(true) // 开启自动播放
.interval(3000) // 3秒切换一次
.indicator(true) // 显示底部指示器

2. Badge 角标组件

Badge 就是我们常见的消息红点、未读数字角标,用来做消息提示、标签标记:

// 数字角标:未读消息
Badge({ count: 5, maxCount: 99 }) {
  Image($r('app.media.icon_message'))
    .width(24)
}
.position({ x: 15, y: -5 })

// 红点角标:无数字提示
Badge({ position: BadgePosition.RightTop }) {
  Image($r('app.media.icon_notify'))
    .width(24)
}

3. 布局进阶

除了基础的 Column/Row,这周还学了两个核心布局:

  • Flex 弹性布局:通过flexDirection控制排列方向,justifyContent主轴对齐,alignItems交叉轴对齐,flexWrap控制换行,是复杂布局的核心
  • Stack 堆叠布局:组件一层叠一层,通过zIndex控制层级,alignContent控制对齐,用来做悬浮按钮、遮罩层、定位元素非常方便

五、学习踩坑总结:这些坑我帮你踩过了

  1. ForEach 用索引当 key,列表渲染错乱:这个是新手必踩坑,一定要用数据本身的唯一 id 当 key,不要图省事用索引
  2. List 没设置高度,无法滚动:一定要给 List 加flexGrow(1)或者固定高度,不然内容超出也不会滚动
  3. @Link 传值忘记加(:给@Link传状态引用的时候,必须加\) 符号,不然传的是值,不是引用,不会双向同步 @Styles 里写组件专属属性,报错:@Styles 只能写通用属性,组件专属属性要写在 @Extend 里
  4. 子组件直接改 @Prop 数据,父组件不同步:记住单向数据流,@Prop 是父传子单向,子组件要改数据要通过事件通知父组件,或者用 @Link

六、学习总结

学完这周的内容,我最大的感受就是:终于从 “能写出页面”,变成了 “能写出规范、高效、可维护的页面”。

我们来复盘一下这周的核心收获:

  1. 掌握了动态列表的开发:List + ForEach,搞定所有列表类页面
  2. 搞懂了状态管理的核心:@State/@Prop/@Link,彻底理解声明式 UI 的数据驱动逻辑
  3. 学会了组件复用:@Styles/@Extend/@Builder,告别重复代码,写优雅的代码
  4. 掌握了高频组件与进阶布局:Swiper、Badge、Flex、Stack,能应对大部分业务场景
Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐