声明式 UI 的本质
在传统的命令式 UI 开发中(如 Android XML + Java,或 iOS UIKit),开发者需要:先创建视图对象,再手动调用 setText()、setVisibility() 等方法来更新界面。每次数据变化,都需要找到对应的视图引用并手动更新,这在复杂应用中极易出现状态与 UI 不同步的 Bug。
ArkUI 采用声明式 UI模式(与 SwiftUI、Jetpack Compose、React 相同的思路):开发者只描述"当前状态下 UI 应该长什么样",框架负责计算差异(diffing)并高效地更新真正需要变化的部分。开发者永远不需要持有视图引用来手动更新。
命令式(旧方式)
- 手动持有视图引用
- 每次数据变化手动调用 set 方法
- 状态与 UI 容易不同步
- 代码冗余,可维护性差
声明式(ArkUI 方式)
- 描述 UI 与状态的映射关系
- 状态变化 → 框架自动更新
- UI 永远是状态的函数
- 逻辑清晰,易于测试
组件基础:@Component 与 @Entry
ArkUI 的所有 UI 均由组件(Component)构成。每个 .ets 文件可以定义一或多个组件。
@Component
将一个 struct(结构体)标记为 ArkUI 组件。标有 @Component 的 struct 必须实现 build() 方法,build() 方法的返回值描述了该组件的 UI 树。
@Entry
标记某个 @Component 为页面入口。每个页面文件只能有一个 @Entry。在 module.json5 的路由表中注册的页面路径,指向的就是含有 @Entry 的文件。
struct(结构体)
ArkUI 组件必须使用 struct 而不是 class。原因是:struct 没有继承关系,组件树的组合通过"包含"而非"继承"实现,这符合声明式 UI 的"组合优于继承"原则。
build() 方法
组件的 UI 描述方法。框架在需要渲染或更新时调用 build(),根据其返回的 UI 树与上次的差异,进行最小化更新。build() 方法必须是纯函数——不能有副作用,不能修改状态。
自定义组件示例
// 自定义可复用组件 —— 用户头像卡片
@Component
struct UserCard {
// @Prop 接收父组件传入的属性(单向数据流,父→子)
@Prop name: string
@Prop avatar: string
@Prop level: number = 1 // 可以有默认值
build() {
Row({ space: 12 }) {
Image(this.avatar)
.width(48)
.height(48)
.borderRadius(24)
.objectFit(ImageFit.Cover)
Column({ space: 4 }) {
Text(this.name)
.fontSize(16)
.fontWeight(FontWeight.Medium)
.fontColor('#e6edf3')
Text(`Lv.${this.level} 开发者`)
.fontSize(12)
.fontColor('#768390')
}
.alignItems(HorizontalAlign.Start)
}
.width('100%')
.padding(16)
.backgroundColor('#1c2128')
.borderRadius(12)
}
}
// 在父组件中使用
@Entry
@Component
struct HomePage {
build() {
Column({ space: 16 }) {
// 使用自定义组件,就像使用内置组件一样
UserCard({
name: '张三',
avatar: '/resources/media/avatar.jpg',
level: 5
})
UserCard({
name: '李四',
avatar: '/resources/media/avatar2.jpg'
// level 不传则使用默认值 1
})
}
.padding(20)
}
}
状态装饰器全览
ArkUI 的状态管理通过装饰器(Decorator)实现。不同的装饰器决定了状态的归属和流向。正确选择装饰器是写出高质量鸿蒙应用的关键:
| 装饰器 | 用途 | 数据流向 | 使用场景 |
|---|---|---|---|
@State | 组件内部私有状态 | 组件内部 | 按钮开关、表单输入、本地计数 |
@Prop | 父组件单向传值 | 父 → 子(单向) | 展示组件接收显示数据 |
@Link | 父子双向绑定 | 父 ↔ 子(双向) | 父子共享可修改状态 |
@Observed | 观察嵌套对象变化 | 对象内部 | 监听对象属性变化 |
@ObjectLink | 接收被观察对象 | 父 ↔ 对象 | 配合 @Observed 使用 |
@Provide | 跨层级提供状态 | 祖先 → 后代 | 主题、用户信息 |
@Consume | 跨层级消费状态 | 后代获取祖先状态 | 配合 @Provide 使用 |
@State — 组件内部状态
@Component
struct Counter {
// @State 使变量成为响应式
// 赋值时框架自动重新调用 build() 进行差量更新
@State count: number = 0
@State isExpanded: boolean = false
build() {
Column({ space: 16 }) {
Text(`计数:${this.count}`).fontSize(24)
Row({ space: 12 }) {
Button('-')
.onClick(() => { if (this.count > 0) this.count-- })
Button('+')
.onClick(() => { this.count++ })
}
}
}
}
@Prop 与 @Link 的区别
// @Prop:父传子,子不能修改父的原值(单向)
@Component
struct DisplayChild {
@Prop value: number // 接收父的值,修改不影响父
build() {
Text(`显示:${this.value}`)
}
}
// @Link:父子双向绑定,子修改会同步到父(双向)
@Component
struct EditChild {
@Link value: number // 与父的变量是同一个引用
build() {
Button('子组件+1')
.onClick(() => {
this.value++ // 父组件中对应的 @State 也会更新
})
}
}
// 父组件
@Entry
@Component
struct Parent {
@State num: number = 0
build() {
Column({ space: 16 }) {
Text(`父:${this.num}`)
// @Prop 传值:子修改自己的副本,父不受影响
DisplayChild({ value: this.num })
// @Link 传值:注意用 $num($ 前缀表示传引用)
EditChild({ value: $num })
}
}
}
$ 前缀语法
在 ArkUI 中,
$变量名 表示传递变量的"引用绑定"而非值。当你要使用 @Link 时,父组件传参必须用 $ 前缀,这告诉框架子组件要与父共享同一个状态对象。
核心布局容器
ArkUI 提供了丰富的布局容器,理解每种容器的排列规则是布局的基础:
Column 与 Row
// Column:垂直布局(子组件从上到下排列)
Column({ space: 16 }) { // space 是子组件间距
Text('第一行')
Text('第二行')
Text('第三行')
}
.width('100%')
.alignItems(HorizontalAlign.Center) // 子组件水平对齐
.justifyContent(FlexAlign.SpaceBetween) // 子组件垂直分布
// Row:水平布局(子组件从左到右排列)
Row({ space: 12 }) {
Image($r('app.media.icon')).width(40).height(40)
Text('应用名称').fontSize(18)
Blank() // 弹性空间,把后面的内容推到右边
Button('设置').fontSize(14)
}
.width('100%')
.height(56)
.padding({ left: 16, right: 16 })
.alignItems(VerticalAlign.Center) // 子组件垂直对齐
Stack 叠加布局
// Stack:子组件叠加在同一位置,后面的组件叠在前面的上层
Stack({ alignContent: Alignment.BottomEnd }) {
// 底层:背景图
Image($r('app.media.banner'))
.width('100%')
.height(200)
.objectFit(ImageFit.Cover)
// 中层:渐变遮罩
Column()
.width('100%')
.height(80)
.linearGradient({
angle: 180,
colors: [[0x00000000, 0], [0xFF000000, 1]]
})
// 顶层:文字标签(右下角)
Text('热门推荐')
.fontSize(16)
.fontColor('#ffffff')
.padding({ left: 12, right: 12, bottom: 12 })
}
Grid 网格布局
@State items: string[] = ['A', 'B', 'C', 'D', 'E', 'F']
// Grid:网格布局
Grid() {
ForEach(this.items, (item: string) => {
GridItem() {
Text(item)
.fontSize(20)
.fontWeight(FontWeight.Bold)
.textAlign(TextAlign.Center)
.width('100%')
.height('100%')
.backgroundColor('#1c2128')
.borderRadius(8)
}
})
}
.columnsTemplate('1fr 1fr 1fr') // 3列等宽
.rowsGap(12)
.columnsGap(12)
.width('100%')
.height(300)
常用内置组件
Text 与 TextInput
@State inputText: string = ''
// Text 丰富的文本样式
Text('鸿蒙 HarmonyOS')
.fontSize(20)
.fontWeight(FontWeight.Bold)
.fontColor('#CF0A2C')
.fontStyle(FontStyle.Italic)
.textDecoration({ type: TextDecorationType.Underline })
.maxLines(2)
.textOverflow({ overflow: TextOverflow.Ellipsis })
// TextInput 输入框
TextInput({ placeholder: '请输入搜索内容', text: this.inputText })
.type(InputType.Normal)
.placeholderColor('#768390')
.placeholderFont({ size: 14 })
.fontSize(16)
.fontColor('#e6edf3')
.backgroundColor('#21262d')
.borderRadius(8)
.onChange((value: string) => {
this.inputText = value
})
List 高性能列表
interface ArticleItem {
id: number
title: string
summary: string
time: string
}
@State articles: ArticleItem[] = [
{ id: 1, title: 'ArkUI 入门', summary: '声明式 UI 基础', time: '2h ago' },
{ id: 2, title: 'Stage 模型详解', summary: 'Ability 生命周期', time: '1d ago' },
]
// List 是支持大量数据的高性能滚动列表
// 只渲染可见区域的 ListItem(类似 RecyclerView)
List({ space: 12 }) {
ForEach(this.articles, (article: ArticleItem) => {
ListItem() {
ArticleCard({ item: article }) // 自定义卡片组件
}
.swipeAction({ // 左滑删除
end: this.DeleteButton(article.id)
})
}, (article: ArticleItem) => article.id.toString())
// ForEach 第三个参数是 key 生成器,用于高效 diff
}
.width('100%')
.divider({ strokeWidth: 1, color: '#21262d' })
.edgeEffect(EdgeEffect.Spring) // 回弹效果
@Builder
DeleteButton(id: number) {
Button('删除')
.backgroundColor('#CF0A2C')
.onClick(() => {
this.articles = this.articles.filter(a => a.id !== id)
})
}
@Builder 函数
@Builder 是 ArkUI 的 UI 构建函数装饰器。它可以让你将可复用的 UI 片段提取为一个函数,而无需创建完整的 @Component 组件。适合那些需要复用但逻辑较简单的 UI 块:
@Entry
@Component
struct ProfilePage {
@State username: string = '鸿蒙开发者'
// @Builder 定义可复用的 UI 片段
@Builder
SectionHeader(title: string, icon: string) {
Row({ space: 8 }) {
Text(icon).fontSize(18)
Text(title)
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor('#e6edf3')
}
.padding({ top: 20, bottom: 8 })
}
build() {
Column() {
// 调用 @Builder 就像调用函数一样
this.SectionHeader('个人信息', '👤')
Text(this.username)
this.SectionHeader('我的项目', '📦')
Text('项目列表...')
this.SectionHeader('设置', '⚙️')
}
}
}
@Styles 与 @Extend
ArkUI 提供了两种复用样式的方式,避免重复书写相同的属性链:
// @Styles:复用通用属性(不针对特定组件类型)
@Styles
function cardStyle() {
.backgroundColor('#1c2128')
.borderRadius(12)
.padding(16)
.width('100%')
}
// @Extend:复用特定组件类型的属性
@Extend(Text)
function titleStyle() {
.fontSize(18)
.fontWeight(FontWeight.Bold)
.fontColor('#e6edf3')
}
@Extend(Text)
function subtitleStyle() {
.fontSize(13)
.fontColor('#768390')
}
// 使用时代码更简洁
@Entry
@Component
struct ArticleCard {
build() {
Column({ space: 8 }) {
Text('ArkUI 声明式 UI 详解').titleStyle()
Text('了解装饰器、布局容器和状态管理').subtitleStyle()
}
.cardStyle() // 应用 @Styles 样式
}
}
组件生命周期
ArkUI 组件有完整的生命周期钩子,用于在特定时机执行逻辑:
@Entry
@Component
struct LifecycleDemo {
@State message: string = '等待中'
// 组件即将出现时调用(挂载前)
aboutToAppear() {
console.log('组件即将出现,适合初始化数据')
this.loadData()
}
// 组件即将消失时调用(卸载前)
aboutToDisappear() {
console.log('组件即将消失,适合取消订阅、释放资源')
this.cancelSubscription()
}
// 页面即将显示(@Entry 专有)
onPageShow() {
console.log('页面显示(从其他页面返回时也会触发)')
}
// 页面即将隐藏(@Entry 专有)
onPageHide() {
console.log('页面隐藏')
}
async loadData() {
this.message = '加载中...'
// 模拟异步加载
await delay(1000)
this.message = '加载完成'
}
build() {
Text(this.message).fontSize(18)
}
}
注意:build() 中禁止副作用
build() 方法会被框架频繁调用(每次状态变化都可能触发),绝对不要在 build() 中发起网络请求、修改状态变量、或执行耗时操作。这类初始化逻辑应放在
aboutToAppear() 中。