做 HarmonyOS 应用时,页面开发最先接触到的通常不是复杂的系统能力,而是 ArkUI。ArkUI 的核心特点是声明式:开发者描述页面在某个状态下应该长什么样,框架负责把状态变化同步到界面上。
这和传统命令式 UI 的思路不同。命令式写法更像是一步步操作控件:创建按钮、设置文字、监听点击、手动改颜色。声明式写法更关注结果:当前状态是什么,界面就按照这个状态渲染。掌握这个思路后,后续学习组件化、状态管理、列表、动画都会顺很多。
本文从一个可运行的页面出发,讲清 ArkUI 的基本结构、组件声明方式、布局链式调用、事件绑定和状态驱动更新。
在 Stage 模型工程中,页面通常放在 entry/src/main/ets/pages 目录下。一个最简单的 ArkUI 页面大致如下:
@Entry
@Component
struct Index {
build() {
Column() {
Text('Hello HarmonyOS')
.fontSize(24)
.fontWeight(FontWeight.Bold)
Button('开始')
.margin({ top: 16 })
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center)
.alignItems(HorizontalAlign.Center)
}
}这里有几个关键点:
@Entry 表示这是当前页面入口组件,页面文件通常只需要一个入口组件。@Component 表示 struct Index 是一个 ArkUI 自定义组件。build() 是组件的 UI 描述函数,页面结构写在这里。Column()、Text()、Button() 都是声明式组件调用。.fontSize()、.margin()、.width() 这类链式方法用于设置属性和样式。ArkUI 代码看起来像是在创建对象,但更准确地说,它是在描述一棵 UI 树。状态变化后,框架会根据新的描述刷新需要变化的部分。
ArkUI 里的组件可以粗略分成两类:基础组件和容器组件。
基础组件负责展示或交互,比如:
Text:展示文本。Image:展示图片。Button:触发点击行为。TextInput:输入文本。容器组件负责组织子组件,比如:
Column:纵向排列。Row:横向排列。Stack:层叠排列。Flex:弹性布局。例如一个常见的信息卡片可以这样写:
@Component
struct ProfileCard {
build() {
Row() {
Image($r('app.media.profile_avatar'))
.width(56)
.height(56)
.borderRadius(28)
Column() {
Text('HarmonyOS Developer')
.fontSize(18)
.fontWeight(FontWeight.Medium)
Text('ArkTS / ArkUI / Stage Model')
.fontSize(13)
.fontColor('#666666')
.margin({ top: 4 })
}
.alignItems(HorizontalAlign.Start)
.margin({ left: 12 })
}
.padding(16)
.width('100%')
.borderRadius(12)
.backgroundColor('#F5F7FA')
}
}这段代码体现了声明式 UI 的基本组织方式:用容器表达结构,用基础组件表达内容,再通过链式属性补齐视觉细节。
build() 的职责是描述 UI,不适合塞大量业务逻辑。实际项目中建议遵守三个原则:
build() 中,但复杂区域应该拆成子组件或 @Builder 方法。build() 中。例如下面这种写法能跑,但随着页面变复杂会很难维护:
build() {
Column() {
if (this.score > 90) {
Text('优秀')
.fontColor('#0A7F38')
} else if (this.score > 60) {
Text('通过')
.fontColor('#B7791F')
} else {
Text('待提升')
.fontColor('#C53030')
}
}
}更适合长期维护的方式是把展示文案和颜色封装成计算结果:
@Component
struct ScoreBadge {
@Prop score: number
private getLabel(): string {
if (this.score > 90) {
return '优秀'
}
if (this.score > 60) {
return '通过'
}
return '待提升'
}
private getColor(): string {
if (this.score > 90) {
return '#0A7F38'
}
if (this.score > 60) {
return '#B7791F'
}
return '#C53030'
}
build() {
Text(this.getLabel())
.fontSize(14)
.fontColor(this.getColor())
.padding({ left: 10, right: 10, top: 4, bottom: 4 })
.borderRadius(10)
.backgroundColor('#FFFFFF')
}
}页面声明会变得更干净,测试和复用也更容易。
ArkUI 组件常通过链式调用设置属性。大多数情况下,属性顺序不影响最终结果,但为了可读性,团队里最好形成固定顺序。
建议按下面的顺序组织:
width、height、constraintSize。padding、margin。layoutWeight、alignSelf、justifyContent、alignItems。fontSize、fontWeight、fontColor、maxLines。backgroundColor、borderRadius、border、shadow。enabled、onClick、onChange。示例:
Button('提交')
.width('100%')
.height(44)
.margin({ top: 24 })
.fontSize(16)
.fontWeight(FontWeight.Medium)
.backgroundColor('#0A59F7')
.borderRadius(8)
.onClick(() => {
this.submit()
})统一顺序不是语法要求,而是工程要求。代码评审时,稳定的样式顺序能明显降低阅读成本。
声明式 UI 的价值在状态变化时最明显。下面做一个计数器页面:
@Entry
@Component
struct CounterPage {
@State count: number = 0
build() {
Column() {
Text(`当前计数:${this.count}`)
.fontSize(28)
.fontWeight(FontWeight.Bold)
Row() {
Button('减少')
.onClick(() => {
if (this.count > 0) {
this.count--
}
})
Button('增加')
.margin({ left: 12 })
.onClick(() => {
this.count++
})
}
.margin({ top: 20 })
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center)
.alignItems(HorizontalAlign.Center)
}
}@State 修饰的字段属于组件内部状态。当 this.count 改变后,使用到它的 UI 会自动刷新。开发者不需要手动查找某个文本控件,也不需要主动调用刷新方法。
这个模式是 ArkUI 开发的核心:
ArkUI 通过 .onClick()、.onChange() 等方法绑定事件。事件函数可以直接写箭头函数,但不要把复杂流程都写进去。
可以这样写:
Button('保存')
.onClick(() => {
this.saveProfile()
})然后把具体流程放到方法里:
private saveProfile(): void {
const name = this.name.trim()
if (name.length === 0) {
this.message = '请输入昵称'
return
}
this.message = '保存成功'
}这样做有两个好处:一是 UI 声明更清晰,二是后续加入埋点、表单校验、异步保存时不会把页面结构搅乱。
下面用 ArkUI 写一个简单登录表单,包含输入、按钮、状态提示和基础校验。
@Entry
@Component
struct LoginPage {
@State account: string = ''
@State password: string = ''
@State message: string = ''
@State loading: boolean = false
private canSubmit(): boolean {
return this.account.trim().length > 0 && this.password.length >= 6 && !this.loading
}
private submit(): void {
if (!this.canSubmit()) {
this.message = '请填写账号,并输入至少 6 位密码'
return
}
this.loading = true
this.message = '正在登录...'
setTimeout(() => {
this.loading = false
this.message = '登录成功'
}, 800)
}
build() {
Column() {
Text('账号登录')
.fontSize(26)
.fontWeight(FontWeight.Bold)
.width('100%')
TextInput({ placeholder: '请输入账号', text: this.account })
.width('100%')
.height(44)
.margin({ top: 24 })
.onChange((value: string) => {
this.account = value
})
TextInput({ placeholder: '请输入密码', text: this.password })
.type(InputType.Password)
.width('100%')
.height(44)
.margin({ top: 12 })
.onChange((value: string) => {
this.password = value
})
Button(this.loading ? '提交中...' : '登录')
.width('100%')
.height(44)
.margin({ top: 20 })
.enabled(this.canSubmit())
.opacity(this.canSubmit() ? 1 : 0.5)
.onClick(() => {
this.submit()
})
if (this.message.length > 0) {
Text(this.message)
.fontSize(13)
.fontColor('#666666')
.margin({ top: 12 })
.width('100%')
}
}
.width('100%')
.height('100%')
.padding(24)
.justifyContent(FlexAlign.Center)
.backgroundColor('#FFFFFF')
}
}这段示例虽然简单,但已经覆盖了 ArkUI 页面开发的主要动作:
@State 保存页面状态。实际项目中可以继续把表单项、按钮、提示条拆成独立组件,让页面只负责组合和流程控制。
ArkUI 支持在 build() 中使用 if 做条件渲染。它适合处理加载态、空态、错误态、权限态等页面分支。
build() {
Column() {
if (this.loading) {
LoadingProgress()
.width(36)
.height(36)
} else if (this.errorMessage.length > 0) {
Text(this.errorMessage)
.fontColor('#C53030')
} else {
Text('内容加载完成')
.fontColor('#222222')
}
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center)
.alignItems(HorizontalAlign.Center)
}写条件渲染时要注意:不要让同一个状态在多个分支里重复判断太多次。页面状态最好先抽象成明确的 UI 状态,例如 loading、errorMessage、data,否则后续维护时容易出现分支冲突。
新手常见的问题是所有内容都写在一个页面里。页面小的时候没问题,但当代码超过几百行,build() 会迅速变成难以维护的长函数。
拆组件时可以参考这些边界:
例如把登录按钮拆成组件:
@Component
struct PrimaryButton {
@Prop text: string
@Prop disabled: boolean
onTap: () => void = () => {}
build() {
Button(this.text)
.width('100%')
.height(44)
.enabled(!this.disabled)
.opacity(this.disabled ? 0.5 : 1)
.backgroundColor('#0A59F7')
.borderRadius(8)
.onClick(() => {
this.onTap()
})
}
}页面里就可以这样使用:
PrimaryButton({
text: this.loading ? '提交中...' : '登录',
disabled: !this.canSubmit(),
onTap: () => {
this.submit()
}
})拆分的目标不是追求组件数量,而是让每个文件的职责更清楚。
不要在 build() 里直接修改 @State。build() 会随着状态变化重新执行,如果在里面继续改状态,容易造成重复刷新甚至循环刷新。
不推荐:
build() {
Column() {
Text('示例')
}
this.count++
}状态修改应该发生在事件、生命周期方法或业务回调中。
如果一个组件的属性链里到处都是复杂三元表达式,说明状态没有整理好。
不推荐:
Text(this.userInfo && this.userInfo.name ? this.userInfo.name : '未登录')
.fontColor(this.userInfo && this.userInfo.vipLevel > 0 ? '#B7791F' : '#666666')更清晰的方式是提前把展示字段整理好:
private getDisplayName(): string {
return this.userInfo?.name ?? '未登录'
}
private getNameColor(): string {
return this.userInfo?.vipLevel > 0 ? '#B7791F' : '#666666'
}然后在 UI 中直接使用:
Text(this.getDisplayName())
.fontColor(this.getNameColor())刚开始写 ArkUI 时,不建议一上来就封装非常通用的万能组件。通用组件需要稳定的调用场景,否则参数会越加越多,最后比直接写页面更难理解。
更实用的做法是:先写清楚页面,看到重复,再抽组件;抽出来后保持接口简单。
在真实项目里,可以把 ArkUI 页面按下面方式组织:
entry/src/main/ets/
├── pages/
│ └── LoginPage.ets
├── components/
│ ├── PrimaryButton.ets
│ ├── FormField.ets
│ └── StateTips.ets
├── models/
│ └── UserProfile.ets
└── services/
└── AuthService.ets页面负责组合组件和处理页面级状态;组件负责展示和局部交互;服务层负责业务流程和外部能力调用。这样划分后,ArkUI 的声明式页面不会和业务逻辑互相缠绕。
ArkUI 声明式 UI 的学习重点不在记住每一个组件属性,而在建立正确的页面组织方式:用状态描述界面,用事件改变状态,用组件拆分复杂结构。
写鸿蒙页面时,可以先抓住这条主线:
build() 只负责描述当前状态下的界面。@State 变化会驱动界面刷新。把这些基础打稳,再去学习列表、动画、跨页面状态和复杂架构时,就不会只是在堆组件,而是在构建可维护的鸿蒙应用界面。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。