HarmonyOS社交通讯应用开发8:ArkUI 声明式开发范式
引言
ArkUI 是 HarmonyOS 的 UI 开发框架,提供声明式开发范式:开发者用"描述 UI 长什么样"的方式写界面,而不是一步步命令界面"怎么画"。上一篇文章我们认识了 ArkTS 与装饰器,本篇把它们串起来:从组件树、状态驱动、事件响应到资源引用,完整拆解一个真实页面。
本篇的目标是:读懂 entry/src/main/ets/pages/Index.ets 的每一行,并借此掌握 ArkUI 常用组件的使用方法。

读完本篇,你将收获:
- 理解声明式 UI 与命令式 UI 的本质区别;
- 掌握 ArkUI 的组件树结构与
build()方法; - 掌握常用布局组件(
Navigation、Flex、Column、Stack)与基础组件(Text、Button、SymbolGlyph); - 理解状态驱动的 UI 刷新机制;
- 掌握事件绑定(
onClick、onAppear)与资源引用$r()/$rawfile()。
一、声明式 UI 与命令式 UI
先建立一个心智模型。传统命令式 UI(如早期的 Canvas 绘制)是这样工作的:
"画一个按钮 → 设置它的文字 → 设置它的颜色 → 等用户点击后再改文字"
而声明式 UI 是这样工作的:
"这里有一个 Button,它的文字绑定到变量 btnText;btnText 变了,UI 自动变"
**声明式的核心是"数据与 UI 绑定"**:开发者只描述"在什么数据下 UI 应该是什么样",状态变化后的更新由框架完成。ArkUI 中这一机制由状态装饰器驱动:@State、@StorageLink 修饰的变量变化时,依赖它们的 UI 自动刷新。这也是上一篇反复强调状态装饰器的原因——它们是声明式范式的"动力源"。
二、页面骨架:@Entry + @Component + build()
每个 ArkUI 页面都是一个"组件"。Index.ets 的骨架:
@Entry
@Component
struct Index {
// 状态区:声明组件依赖的数据(变化触发 UI 刷新)
@StorageLink('pageInfos') pageInfos: NavPathStack = new NavPathStack();
@StorageLink('bottomHeight') bottomHeight: number = 0;
...
// UI 区:描述"数据长什么样,界面就长什么样"
build() {
...
}
}
三个要点:
struct是组件容器:Index是结构体(struct),被@Entry @Component装饰后成为页面组件;- 状态区:成员变量用状态装饰器修饰,它们的变化会触发
build()重新执行、UI 局部刷新; build()方法:描述 UI 树,是每个组件必须实现的方法。
组件树是理解 build() 的关键:UI 是一棵"容器套容器、容器套叶子组件"的树。Index 的组件树如下:
Index (页面)
└── Navigation(pageInfos) // 导航容器
└── Flex(Column方向) // 弹性布局容器
├── Text(title) // 标题文本
└── Column(space:12) // 垂直容器
├── Button(内容编辑) // 按钮1
└── Button(内容浏览) // 按钮2
三、Navigation 与路由:页面的"骨架"
Index.ets 用 Navigation 作为根容器:
// entry/src/main/ets/pages/Index.ets(节选)
build() {
Navigation(this.pageInfos) { // 传入全局导航栈,管理页面跳转
...
}
.onAppear(() => {
// 页面出现时检查分享链接,有则跳转
this.isGesturesShareLinkChanged();
this.isKnockShareLinkChanged();
})
.hideTitleBar(true) // 隐藏系统标题栏
.mode(NavigationMode.Stack) // 栈式导航模式
.backgroundColor($r('sys.color.container_modal_unfocus_background')) // 背景色(系统资源)
}
Navigation(this.pageInfos):导航容器,pageInfos是NavPathStack类型——导航栈对象。所有子页面通过pushPathByName/replacePath等操作进出这个栈;- **
.mode(NavigationMode.Stack)**:栈式导航(一个页面盖一个页面,返回时出栈); - **
.onAppear()**:页面显示回调——这里被用来"页面一出现就检查是否有分享链接待跳转",配合@Watch逻辑(第 7 篇讲过); $r('sys.color.xxx'):引用系统资源(下节详述)。
导航跳转在按钮事件里:
Button($r('app.string.button2'))
.width('100%')
.constraintSize({ maxWidth: 448 })
.onClick(async () => {
// 按路由名跳转:ContentBrowsePage 在 route_map.json 中注册
this.pageInfos.pushPathByName('ContentBrowsePage', null, true);
})
pushPathByName(路由名, 参数, 是否动画) 的第一个参数必须与 route_map.json 中注册的 name 一致——这就是第 4 篇说的"配置与代码的契约"。
四、布局组件:Flex 与 Column
布局组件决定"子组件怎么排"。Index.ets 用了两个:
Flex({ direction: FlexDirection.Column, justifyContent: FlexAlign.SpaceBetween }) {
Text($r('app.string.title'))
.fontSize(30)
.textAlign(TextAlign.Start)
.width('100%')
.fontWeight(FontWeight.Bold)
.padding({ top: 56 })
Column({ space: 12 }) {
Button($r('app.string.button1')) { ... }
Button($r('app.string.button2')) { ... }
}
.width('100%')
.alignSelf(ItemAlign.End)
.padding({ bottom: 16 })
}
.width('100%')
.height('100%')
.padding({
left: this.currentBreakpoint === WidthBreakpoint.WIDTH_LG ? 32 : 24,
right: this.currentBreakpoint === WidthBreakpoint.WIDTH_LG ? 32 : 24,
bottom: this.bottomHeight
})
逐个讲解:
Flex({ direction, justifyContent }):弹性布局。FlexDirection.Column表示子组件纵向排列;FlexAlign.SpaceBetween表示主轴方向两端对齐、间距自动分配——所以标题在上、按钮组在下,实现了"标题顶部、按钮底部"的经典页面结构;- **
Column({ space: 12 })**:纵向容器,space为子组件间距 12vp。两个按钮并排纵列; - 链式调用(.width()/.padding()/...):ArkUI 的属性方法风格,每个
.方法()给组件加一条属性,可无限串联; - **
.alignSelf(ItemAlign.End)**:子组件在交叉轴(横向)上靠右对齐; - 三元表达式:
.padding({ left: this.currentBreakpoint === WidthBreakpoint.WIDTH_LG ? 32 : 24 })——根据断点状态动态取内边距,大屏(lg)32、小屏 24,这就是响应式适配在代码层面的体现(断点值由BreakpointSystem.ets计算、经@StorageLink('breakpoint')同步而来)。
同样值得留意的还有 ContentBrowsePage.ets 中的 Stack(堆叠布局,子组件层叠):
// entry/src/main/ets/view/contentBrowse/ContentBrowsePage.ets(节选)
@Builder
addTabBar(icon: Resource) {
Stack() { // 堆叠:按钮底座在下,图标在上
Button()
.width(40)
.height(40)
.borderRadius(20) // 圆形按钮
SymbolGlyph(icon) // 系统符号图标
.fontSize(24)
.fontColor([Color.White])
.renderingStrategy(SymbolRenderingStrategy.SINGLE)
}
}
五、基础组件:Text 与 Button
Text —— 文本组件:
Text($r('app.string.title'))
.fontSize(30) // 字号 30fp
.textAlign(TextAlign.Start) // 左对齐
.width('100%') // 撑满父容器宽度
.fontWeight(FontWeight.Bold) // 粗体
.padding({ top: 56 }) // 顶部内边距 56vp
注意 Text 的参数是资源引用 $r('app.string.title') 而非写死的字符串——zh_CN/element/string.json 中 title 的值为"社交通讯全场景协同",base/en_US 中为"Full-Scenario Collaboration in Social Communication"。应用切换系统语言后,界面文案自动切换,这就是资源引用的威力。
Button —— 按钮组件:
Button($r('app.string.button1')) // 按钮文字同样用资源引用("内容编辑")
.width('100%')
.constraintSize({ maxWidth: 448 }) // 约束最大宽度:平板/电脑上不至于过宽
.onClick(async () => {
this.pageInfos.pushPathByName('ContentEditorPage', null, true); // 跳转编辑页
})
Button(内容):参数是按钮文案;.constraintSize({ maxWidth: 448 }):尺寸约束——宽度 100% 的同时不超过 448vp,兼顾手机与平板;.onClick(回调):事件绑定,点击时执行箭头函数。这是 ArkUI 事件体系最常见的入口(其他还有onAppear、onTouch等)。
SymbolGlyph —— 系统符号图标。上一节的 addTabBar 示例里出现过它,这里单独展开:SymbolGlyph 用于显示系统符号库中的矢量图标,与 $r('sys.symbol.xxx') 配套使用。它和位图(Image)的关键区别在于矢量可缩放、颜色可编程:fontSize 控制图标大小,fontColor 可传颜色数组实现多色渲染,renderingStrategy 控制渲染策略(SymbolRenderingStrategy.SINGLE 为单色)。本项目底部页签栏就大量使用了这套能力——HomeConstants.FOOTER_TOPIC_ICONS 中存了 house_fill、bag_fill_1、plus、ellipsis_message_fill、person_crop_circle_fill_1 五个系统符号,ContentBrowsePage.ets 再通过 SymbolGlyphModifier 为普通态与选中态分别指定 fontColor(选中态用 ohos_id_color_text_primary_activated 强调色),实现"选中高亮"的交互效果,全程不需要准备多套不同颜色的位图——这正是系统符号资源的设计初衷。
六、资源引用体系:$r() 与 $rawfile()
ArkUI 的资源引用有两种形式,本项目都用到了:
1. $r('app.xxx.yyy') / $r('sys.xxx.yyy')
Text($r('app.string.title')) // 应用资源:字符串(resources/base/element/string.json)
.backgroundColor($r('sys.color.container_modal_unfocus_background')) // 系统资源:颜色
$r('sys.symbol.house_fill') // 系统资源:符号图标(HomeConstants 中)
app前缀:应用自身资源,如app.string.title(字符串)、app.color.xxx(颜色)、app.media.xxx(图片);sys前缀:系统预置资源,如sys.color.xxx(系统色板)、sys.symbol.xxx(系统符号图标库)——好处是自动适配深色模式与主题;$r()返回Resource类型,可直接传给组件参数或存入Resource[](如HomeConstants.FOOTER_TOPIC_ICONS)。
2. $rawfile()
Image($rawfile('waterflow_item_img0.jpg')) // 引用 rawfile 目录中的图片
$rawfile() 引用 entry/src/main/resources/rawfile/ 下的原始文件(本项目放了 8 张瀑布流示例图与 2 个 mock 数据 JSON)。rawfile 资源不参与国际化与编译处理,原样打包,适合放静态图片、音频、模板文件。
七、状态驱动:一条完整的"数据流"
把第 6、7 篇与本篇的知识串起来,看 Index.ets 如何被"状态"驱动:
【数据源 1:接续/分享】 【数据源 2:窗口】
EntryAbility.onCreate 解析链接 EntryAbility.onWindowStageCreate
│ 写入 │ 计算
▼ ▼
AppStorage: GesturesShare_* / continuePageUrl AppStorage: breakpoint / bottomHeight
│ 绑定 │ 绑定
▼ ▼
Index 的 @StorageLink/@Watch 感知变化 Index 的 currentBreakpoint / bottomHeight
│ 回调 │ 使用
▼ ▼
pageInfos.replacePath → 直达详情页 padding 动态取值 → 响应式布局
这就是本项目 UI 层的完整机制:数据写进 AppStorage,页面通过状态装饰器绑定,变化自动触发 UI 更新。开发者的工作就是"维护数据 + 描述 UI",中间的刷新由框架完成——这正是声明式范式的精髓。
八、小结
本篇以 Index.ets 为主样例,覆盖了 ArkUI 声明式范式的核心:
- 范式:声明式 UI = 数据 + 绑定 + 描述,状态变化自动刷新,区别于命令式的"逐步绘制";
- 骨架:
@Entry @Component struct+build()描述组件树; - 布局:
Navigation(导航容器 + NavPathStack 栈)、Flex(弹性布局)、Column(纵向容器)、Stack(堆叠),属性用链式方法设置; - 组件:
Text(文本)、Button(按钮 + onClick 事件)、SymbolGlyph(系统符号); - 状态驱动:
@StorageLink/@StorageProp/@Watch与 AppStorage 构成全局数据流,replacePath/pushPathByName实现跳转; - 资源:
$r('app.xxx')应用资源、$r('sys.xxx')系统资源、$rawfile()原始文件,文案不写死、多语言自动切换。
模块一结语
至此,模块一《项目总览与工程基础》的八篇文章全部完成。回顾整个学习路径:
- 01 项目总览:建立全局认知,认识六大场景与工程结构;
- 02 场景演示与运行环境:知道怎么跑、需要什么环境;
- 03 工程目录全景解析:掌握代码地图与分层哲学;
- 04 应用配置文件精读:看懂应用与模块配置(skills、continuable、权限);
- 05 构建体系入门:理解 Hvigor 构建链路与混淆;
- 06 Stage 模型与 UIAbility 生命周期:读懂入口能力的生命周期与接续机制;
- 07 ArkTS 语言基础:掌握类、接口、枚举、装饰器;
- 08 ArkUI 声明式开发范式:掌握声明式 UI、状态驱动与常用组件。
下一步,你可以进入模块二,深入 view/contentEditor 与 view/contentBrowse 两大业务模块的页面实现,看看分布式能力如何在具体页面中落地。
更多推荐


所有评论(0)