为什么写个按钮还要“修仙”?——鸿蒙 ArkUI 框架从底层心法到界面实战你真的懂了吗?
开篇语
哈喽,各位小伙伴们,你们好呀,我是喵手。运营社区:C站/掘金/腾讯云/阿里云/华为云/51CTO;欢迎大家常来逛逛
今天我要给大家分享一些自己日常学习到的一些知识点,并以文字的形式跟大家一起交流,互相学习,一个人虽可以走的更快,但一群人可以走的更远。
我是一名后端开发爱好者,工作日常接触到最多的就是Java语言啦,所以我都尽量抽业余时间把自己所学到所会的,通过文章的形式进行输出,希望以这种方式帮助到更多的初学者或者想入门的小伙伴们,同时也能对自己的技术进行沉淀,加以复盘,查缺补漏。
小伙伴们在批阅的过程中,如果觉得文章不错,欢迎点赞、收藏、关注哦。三连即是对作者我写作道路上最好的鼓励与支持!
前言
第一次上手 ArkUI(ArkTS 声明式 UI)的时候,我的内心是复杂的:语法像写段子,界面像搭乐高,状态一动百川响应。可一到真项目,你会发现——生命周期卡点、事件回调陷阱、样式风格统一这些“人间烟火”,才是决定你代码能不能“发量保全”的关键。今天这篇,我就把ArkUI 的声明式架构 → 生命周期与事件绑定 → 自定义组件与样式管理一条龙讲清楚,顺带塞几个可直接抄走的实战片段,少一点玄学,多一点落地。
目录
- 前言:为什么一定要先换一副“声明式大脑”
- 01|ArkUI 声明式 UI 架构(状态驱动、树对齐、最小重绘)
- 02|组件生命周期与事件绑定(该出手时就出手)
- 03|自定义组件与样式管理(把界面做出“团队味儿”)
- 04|完整小实战:习惯打卡卡片(状态、事件、样式、动画一次用全)
- 05|性能与易踩坑清单(不翻车才是王道)
- 结语:漂亮界面背后,是靠谱的工程秩序
前言:为什么一定要先换一副“声明式大脑”
命令式 UI 的思路是“发生了什么 → 我应该怎么改 DOM/视图”;声明式则是“状态是什么 → 视图就长什么样”。ArkUI 把后者做到极致:你只要维护好状态,框架就负责对齐(reconcile)组件树、计算最小重绘,并在生命周期节点上给你安全插手的机会。好处?心智负担小、可测试性高、并发风险低。
01|ArkUI 声明式 UI 架构
1.1 一眼明白的“状态 → 视图”
@Entry
@Component
struct CounterPage {
@State count: number = 0
build() {
Column() {
Text(`Count: ${this.count}`).fontSize(22).padding(12)
Row() {
Button('-').onClick(() => this.count--)
Button('+').onClick(() => this.count++)
}.justifyContent(FlexAlign.SpaceAround).width('60%')
}.height('100%').justifyContent(FlexAlign.Center)
}
}
要点:@State 改变 → 框架对齐组件树 → 只重建变动分支 → 最小量刷新。你不需要“手动 setXXX 重绘”;ArkUI 会保证可预测的同步渲染顺序。
1.2 状态传递的四大角色
@State:组件内部可变状态;本组件改、仅本组件重建。@Prop:父传子的只读输入;父变则子随之更新。@Link:父子双向联动的“引用态”,适合表单子项回写父级。@Provide/@Consume:跨层级依赖注入(主题、会话、全局设置)。
@Provide theme = { primary: '#0A84FF', radius: 16 }
@Consume theme // 子孙组件取用,无需层层透传
1.3 Reconciliation(对齐)策略的工程意义
- 纯函数式 build:
build()只根据当前状态拼装 UI,不要做副作用(发请求、订阅……)。 - 稳定节点关键属性:
id()、key()可帮助 ArkUI 识别同一逻辑节点,减少无效重建。 - 长列表优化:
List/LazyForEach合理设置cachedCount、sticky,避免“一滑就卡”。
02|组件生命周期与事件绑定
2.1 生命周期时间线(常用)
aboutToAppear():组件初次进入渲染树前后的一瞬;适合一次性初始化(轻量)。onPageShow()/onPageHide():页面级可见性变化(路由返回、进入后台);适合恢复/挂起逻辑。aboutToDisappear():组件即将离开渲染树;释放资源、取消订阅。onBackPress():拦截返回;做“二次确认”或状态保存。
小贴士:IO/网络不要塞进 build。想要请求数据?在
aboutToAppear()中触发,然后用@State驱动视图刷新。
@Component
struct FeedList {
@State items: Article[] = []
private cancel?: () => void
aboutToAppear() {
const { cancel, promise } = fetchFeed() // 你自己的请求封装
this.cancel = cancel
promise.then(list => this.items = list).catch(_ => showToast('Load failed'))
}
aboutToDisappear() {
this.cancel?.()
}
build() {
List() {
ForEach(this.items, (it) => ListItem() { Text(it.title) })
}
}
}
2.2 事件绑定的三条铁律
- 回调是无名英雄:
.onClick()、手势.gesture()都会闭包当前状态;别在回调里引入过期引用(异步更新时注意)。 - 节流/防抖要自带:高频手势(拖拽/滚动)里做 throttle;表单输入做 debounce。
- 副作用有生有死:在
aboutToDisappear()里清理计时器、监听、订阅,别“魂飞魄散”。
Button('Save')
.onClick(async () => {
if (this.saving) return
this.saving = true
try { await api.save(this.form) } finally { this.saving = false }
})
03|自定义组件与样式管理
3.1 自定义组件:把“拼装”变“复用”
// 可复用的卡片容器
@Component
export struct Card {
@Prop title: string
// slot 让父组件塞内容进来
build() {
Column() {
Text(this.title).fontSize(18).fontWeight(FontWeight.Medium).padding({ top: 12, bottom: 8 })
// 内容插槽
// @ts-ignore: 伪示例,具体以版本 API 为准
Slot()
}
.backgroundColor($r('app.color.cardBg'))
.borderRadius($r('app.float.radius'))
.padding(12)
.shadow({ radius: 12, color: '#00000022', offsetY: 4 })
}
}
使用:
Card({ title: 'Profile' }) {
Row() {
Image($r('app.media.avatar')).width(48).height(48).borderRadius(24)
Column() {
Text('Cat Coder').fontSize(16)
Text('“咖啡续命,代码续杯。”').fontColor('#666')
}.margin({ left: 12 })
}
}
3.2 资源化样式:团队协作的“唯一真相源”
把颜色、字号、圆角、间距抽到资源里(resources/base/element/、resources/base/profile/theme.json等),不同主题共享一套 key,按需切皮。
示例:resources/base/profile/theme.json
{
"app": {
"color": {
"primary": "#0A84FF",
"text": "#111111",
"muted": "#666666",
"cardBg": "#FFFFFF",
"cardBgDark": "#1C1C1E"
},
"float": {
"radius": 16,
"gap": 12
}
}
}
在 ArkTS 使用:
.backgroundColor(isDark ? $r('app.color.cardBgDark') : $r('app.color.cardBg'))
.borderRadius($r('app.float.radius'))
.padding($r('app.float.gap'))
3.3 主题切换与全局态
用 @Provide/@Consume 存放主题状态,全局切换一次、全局生效。
@Entry
@Component
struct AppRoot {
@Provide themeMode: 'light' | 'dark' = 'light'
build() {
Column() {
Toggle('Dark mode', this.themeMode === 'dark')
.onChange(v => this.themeMode = v ? 'dark' : 'light')
HomePage()
}
}
}
@Component
struct HomePage {
@Consume themeMode: 'light' | 'dark'
build() {
const isDark = this.themeMode === 'dark'
Column() {
Text('Hello ArkUI').fontColor(isDark ? '#EEE' : '#111')
}.backgroundColor(isDark ? '#000' : '#FFF')
}
}
3.4 动画与微交互(animateTo/Transition)
@State expanded: boolean = false
Card({ title: 'More' }) {
Text(this.expanded ? '收起' : '展开')
}
.onClick(() => {
animateTo({ duration: 200, curve: Curve.EaseInOut }, () => {
this.expanded = !this.expanded
})
})
.height(this.expanded ? 240 : 80)
04|完整小实战:习惯打卡卡片(状态、事件、样式、动画一次拉满)
目标:做一个可复用的“习惯卡片”,支持勾选完成、长按重命名、主题自适配、入场过渡。
// HabitCard.ets
export type Habit = { id: string; name: string; done: boolean }
@Component
export struct HabitCard {
@Link model: Habit
@Consume themeMode: 'light' | 'dark'
@State editing: boolean = false
@State nameDraft: string = ''
private bg() {
return this.themeMode === 'dark' ? $r('app.color.cardBgDark') : $r('app.color.cardBg')
}
aboutToAppear() {
// 入场轻微缩放,提升“活着”的感觉
animateTo({ duration: 160 }, () => {})
}
build() {
Row() {
Checkbox({ name: this.model.name, group: 'habit' })
.select(this.model.done)
.onChange(v => this.model.done = v)
if (!this.editing) {
Text(this.model.name)
.fontSize(16)
.margin({ left: 12 })
.onLongPress(() => {
this.editing = true
this.nameDraft = this.model.name
})
} else {
TextInput({ text: this.nameDraft, placeholder: 'Rename habit' })
.onChange(v => this.nameDraft = v)
.onSubmit(() => this.commit())
}
Blank().layoutWeight(1)
Button(this.editing ? 'Save' : 'Edit')
.onClick(() => this.editing ? this.commit() : (this.editing = true, this.nameDraft = this.model.name))
}
.padding($r('app.float.gap'))
.backgroundColor(this.bg())
.borderRadius($r('app.float.radius'))
.shadow({ radius: 12, color: '#00000022', offsetY: 4 })
.transition(TransitionEffect.OPACITY) // 列表中增加/删除的渐隐渐现
}
private commit() {
const v = this.nameDraft.trim()
if (v.length === 0) { showToast('Name required'); return }
this.model.name = v
this.editing = false
}
}
列表页使用:
@Entry
@Component
struct HabitListPage {
@Provide themeMode: 'light' | 'dark' = 'light'
@State habits: Habit[] = [
{ id: '1', name: 'Drink water', done: false },
{ id: '2', name: 'Walk 5,000 steps', done: true }
]
build() {
Column() {
Row() {
Text('Habits').fontSize(24).fontWeight(FontWeight.Bold)
Blank().layoutWeight(1)
Toggle('Dark', this.themeMode === 'dark')
.onChange(v => this.themeMode = v ? 'dark' : 'light')
}.padding(12)
List({ space: 12 }) {
ForEach(this.habits, (h, i) => ListItem() {
HabitCard({ model: this.habits[i] as any }) // Link 传入
}, (h) => h.id)
}.cachedCount(6).padding(12)
}
}
}
你收获了什么?
@Link让子组件直接改父级数据,简单粗暴但范围要控制。- 文案编辑走长按进入编辑、提交校验,用户体验更顺滑。
- 主题切换、入场过渡、列表缓存,一次性合体。
05|性能与易踩坑清单
5.1 性能 7 招
- 长列表用 Lazy:
List+ForEach时指定cachedCount,避免一次渲染全量。 - 稳定 key:
ForEach(..., item => item.id),减少“整行换新”。 - 轻 build、重副作用管理:
build()只拼 UI;副作用放aboutToAppear(),并在aboutToDisappear()清理。 - 图片策略:大图裁剪+占位骨架;滚动时延迟加载,停下再渲染高清。
- 节流手势:拖拽/滑动事件里做 throttle(如每 16ms 一次)。
- 避免过深组件嵌套:把“含逻辑的小片段”做成扁平可复用组件,比套娃强。
- 动画克制:
animateTo时长 150~250ms 体感最佳;避免在重布局节点上频繁动画。
5.2 易踩坑 7 条
- 在 build 里发请求:会被多次调用,轻则重复请求,重则死锁 UI。
- 忘记清理定时器/订阅:页面切走还在跑,内存、CPU 一起炸。
- Link 滥用:数据可被深层子组件“随手改”,复杂页面请用
@Provide/@Consume或集中管理。 - 回调闭包旧状态:异步回调里用到的状态可能已变,注意取最新值或通过
Link传引用。 - 主题资源硬编码:颜色/间距直接写死字符串,换皮必痛;一律走
$r()。 - 动画卡顿:动画中修改布局重排属性(如宽高)过多;尽量动不触发布局的属性(透明度、变换)。
- 过度抽象:为复用而复用,结果样式与逻辑绑死;组件只做“一件事”,组合胜过继承。
结语:漂亮界面背后,是靠谱的工程秩序
ArkUI 的美,不在“招式花哨”,而在状态即真相、生命周期有秩序、样式有中枢。当你的组件像砖,样式像模数,交互像水,项目就能既开发迅速又长期可维护。下一次同事夸你页面顺滑,别害羞:这不是玄学,是你把工程做对了。😉
一页速记(带走就能用)
- 架构:状态驱动 → build 纯函数 → 框架对齐 → 最小重绘。
- 状态:
State/Prop/Link/Provide&Consume各司其职,别混。 - 生命周期:初始化/订阅 →
aboutToAppear(),清理 →aboutToDisappear(),显隐 →onPageShow/Hide()。 - 事件:回调注意闭包;高频手势节流;副作用有生有死。
- 样式:资源化、主题化、参数化(颜色/间距/圆角);统一 key,统一切换。
- 复用:小而美组件+Slot;Link 谨慎,复杂场景上升到 Provide。
- 性能:长列表 Lazy、稳定 key、轻 build、克制动画、图像延迟加载。
… …
文末
好啦,以上就是我这期的全部内容,如果有任何疑问,欢迎下方留言哦,咱们下期见。
… …
学习不分先后,知识不分多少;事无巨细,当以虚心求教;三人行,必有我师焉!!!
wished for you successed !!!
⭐️若喜欢我,就请关注我叭。
⭐️若对您有用,就请点赞叭。
⭐️若有疑问,就请评论留言告诉我叭。
版权声明:本文由作者原创,转载请注明出处,谢谢支持!
- 点赞
- 收藏
- 关注作者
评论(0)