在这里插入图片描述

摘要

在 HarmonyOS 的 ArkUI 开发中,自定义组件已经成了日常操作。
但很多人在真正做业务时,会慢慢发现一个问题:

组件是写出来了,但不好交互、不好复用、也不好维护

要么事件写死在组件里,父组件完全控制不了;
要么状态到处飞,点一下界面刷新一片;
再复杂一点,组件之间的交互逻辑就开始乱套。

这篇文章就结合实际开发中最常见的场景,系统讲一讲:

ArkUI 的自定义组件,应该如何设计交互,才能既好用又不翻车。

所有示例代码都基于 ArkTS,可直接运行,重点放在“为什么要这么写”。

引言

随着 HarmonyOS 设备形态越来越多,一个页面往往要在:

  • 手机
  • 平板
  • 智慧屏
  • 甚至车机

上同时运行。

这也意味着 UI 组件不能只“看起来能用”,而是要:

  • 行为清晰
  • 状态可控
  • 交互方式统一
  • 能被不同页面、不同设备复用

而 ArkUI 在设计之初,其实已经给了一套比较成熟的思路:

单向数据流 + 状态驱动 UI + 事件参数化

只是很多人在一开始写 Demo 的时候,没有真正用起来。

下面我们从最基础的交互开始,一步一步把这套思路走完整。

ArkUI 自定义组件交互的核心思想

在 ArkUI 中,自定义组件的交互,本质上只有一句话:

组件内部负责“怎么交互”,组件外部决定“交互之后做什么”。

换句话说:

  • 组件内部可以绑定点击、手势、动画
  • 但不要在组件内部写业务逻辑
  • 通过参数把交互“抛出去”

这样组件才是可复用的,而不是一次性的。

最基础的交互:点击事件

定义一个可交互的自定义组件

先从最常见的按钮组件开始。

@Component
export struct MyButton {
  @Prop text: string
  @Prop onTap?: () => void

  build() {
    Text(this.text)
      .padding(12)
      .backgroundColor('#317AF7')
      .fontColor(Color.White)
      .borderRadius(8)
      .onClick(() => {
        this.onTap?.()
      })
  }
}

这里有几个很关键的点:

  • onClick 写在组件内部
  • 但真正的处理逻辑通过 @Prop 暴露给外部
  • 组件本身不关心点了之后干什么

这样这个组件就不会和任何业务强绑定。

在页面中使用这个组件

@Entry
@Component
struct Page {
  build() {
    Column({ space: 16 }) {
      MyButton({
        text: '提交',
        onTap: () => {
          console.info('按钮被点击了')
        }
      })
    }
    .padding(20)
  }
}

这种写法的好处是:

  • 页面逻辑一眼能看懂
  • 组件完全可以被其他页面复用
  • 后期加埋点、权限判断,都在外部做

组件内部的状态交互

有些组件的交互,其实只和自己有关,不需要通知父组件。

示例:可选中组件

@Component
export struct SelectItem {
  @State selected: boolean = false

  build() {
    Text(this.selected ? '已选中' : '未选中')
      .padding(10)
      .backgroundColor(this.selected ? '#4CAF50' : '#CCCCCC')
      .borderRadius(6)
      .onClick(() => {
        this.selected = !this.selected
      })
  }
}

这里用到了 @State,它的作用非常明确:

  • 状态只在组件内部使用
  • 状态变化会自动驱动 UI 刷新

这种组件非常适合:

  • 标签
  • 开关
  • 选项卡里的单个 item

父子组件之间的交互(最常见也最容易写乱)

在真实业务中,更常见的是:

子组件负责触发操作,
父组件负责维护状态。

子组件:只负责“点了”

@Component
export struct CounterItem {
  @Prop count: number
  @Prop onAdd?: () => void

  build() {
    Row({ space: 12 }) {
      Text(`数量:${this.count}`)
      Button('+')
        .onClick(() => {
          this.onAdd?.()
        })
    }
  }
}

子组件只做两件事:

  • 展示数据
  • 把点击事件抛出去

父组件:真正管理数据

@Entry
@Component
struct Page {
  @State count: number = 0

  build() {
    CounterItem({
      count: this.count,
      onAdd: () => {
        this.count++
      }
    })
  }
}

这种写法有一个非常重要的好处:

  • 所有状态集中在父组件
  • 数据流方向是单向的
  • 页面复杂了也不会失控

手势交互:不只是点击

在一些偏交互型的场景,比如卡片、图片、列表项,点击已经不够用了。

长按示例

@Component
export struct LongPressItem {
  build() {
    Text('长按我')
      .padding(20)
      .backgroundColor('#EEEEEE')
      .gesture(
        LongPressGesture()
          .onAction(() => {
            console.info('触发了长按')
          })
      )
  }
}

ArkUI 内置了不少手势类型,比如:

  • TapGesture
  • LongPressGesture
  • PanGesture
  • SwipeGesture

一般建议:

  • 简单点击用 onClick
  • 稍复杂的交互再上 gesture

结合实际场景的应用示例

场景一:列表项点击进入详情页

@Component
export struct ListItem {
  @Prop title: string
  @Prop onSelect?: () => void

  build() {
    Text(this.title)
      .padding(16)
      .onClick(() => {
        this.onSelect?.()
      })
  }
}

这种写法在列表、瀑布流中非常常见,组件本身完全不知道跳哪里。

场景二:可复用的操作按钮组

@Component
export struct ActionBar {
  @Prop onEdit?: () => void
  @Prop onDelete?: () => void

  build() {
    Row({ space: 20 }) {
      Button('编辑').onClick(() => this.onEdit?.())
      Button('删除').onClick(() => this.onDelete?.())
    }
  }
}

在不同页面,只需要传不同的回调即可。

场景三:分布式场景下的状态触发

在分布式场景中,组件更不应该直接操作数据。

@Component
export struct SyncButton {
  @Prop onSync?: () => void

  build() {
    Button('同步数据')
      .onClick(() => {
        this.onSync?.()
      })
  }
}

父组件中再去调用分布式数据能力,这样组件本身依然是“干净的”。

常见问题 Q&A

Q1:组件里能不能直接改父组件的状态?

不推荐。
一旦这么做,组件就和页面强耦合,后期维护会非常痛苦。

Q2:什么时候用 @State,什么时候用 @Prop

简单记忆:

  • 自己用的状态,用 @State
  • 外部传进来的,用 @Prop

Q3:事件参数会不会影响性能?

不会。
ArkUI 本身就是为这种模式设计的,比你乱改状态安全得多。

总结

在 ArkUI 中,把自定义组件交互写清楚,其实并不复杂。

核心就三点:

  • 交互写在组件内部
  • 状态放在合适的位置
  • 通过参数完成通信

当你开始刻意遵守单向数据流之后,就会发现:

  • 组件更好拆
  • 页面更好维护
  • 后期需求变动成本明显降低
Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐