ArkUI 自定义组件到底该怎么交互?一篇写给真实业务的实战总结
摘要 本文系统讲解了HarmonyOS ArkUI中自定义组件的交互设计原则。核心思想是采用"单向数据流+状态驱动UI+事件参数化"模式,实现组件的可复用性和易维护性。文章从基础点击事件入手,逐步深入到组件状态管理、父子组件通信、手势交互等常见场景,强调组件内部处理交互方式,外部通过参数控制业务逻辑。通过具体代码示例展示了如何设计清晰可控的组件交互,包括列表项、操作按钮组等典型

摘要
在 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 中,把自定义组件交互写清楚,其实并不复杂。
核心就三点:
- 交互写在组件内部
- 状态放在合适的位置
- 通过参数完成通信
当你开始刻意遵守单向数据流之后,就会发现:
- 组件更好拆
- 页面更好维护
- 后期需求变动成本明显降低
更多推荐



所有评论(0)