引言

ArkUI 是 HarmonyOS 的 UI 开发框架,提供声明式开发范式:开发者用"描述 UI 长什么样"的方式写界面,而不是一步步命令界面"怎么画"。上一篇文章我们认识了 ArkTS 与装饰器,本篇把它们串起来:从组件树、状态驱动、事件响应到资源引用,完整拆解一个真实页面。

本篇的目标是:读懂 entry/src/main/ets/pages/Index.ets 的每一行,并借此掌握 ArkUI 常用组件的使用方法。

img

读完本篇,你将收获:

  • 理解声明式 UI 与命令式 UI 的本质区别;
  • 掌握 ArkUI 的组件树结构与 build() 方法;
  • 掌握常用布局组件(NavigationFlexColumnStack)与基础组件(TextButtonSymbolGlyph);
  • 理解状态驱动的 UI 刷新机制;
  • 掌握事件绑定(onClickonAppear)与资源引用 $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() {
    ...
  }
}

三个要点:

  1. struct 是组件容器Index 是结构体(struct),被 @Entry @Component 装饰后成为页面组件;
  2. 状态区:成员变量用状态装饰器修饰,它们的变化会触发 build() 重新执行、UI 局部刷新;
  3. build() 方法:描述 UI 树,是每个组件必须实现的方法。

组件树是理解 build() 的关键:UI 是一棵"容器套容器、容器套叶子组件"的树。Index 的组件树如下:

Index (页面)
└── Navigation(pageInfos)          // 导航容器
    └── Flex(Column方向)           // 弹性布局容器
        ├── Text(title)            // 标题文本
        └── Column(space:12)       // 垂直容器
            ├── Button(内容编辑)    // 按钮1
            └── Button(内容浏览)    // 按钮2

三、Navigation 与路由:页面的"骨架"

Index.etsNavigation 作为根容器:

// 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):导航容器,pageInfosNavPathStack 类型——导航栈对象。所有子页面通过 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.jsontitle 的值为"社交通讯全场景协同",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 事件体系最常见的入口(其他还有 onAppearonTouch 等)。

SymbolGlyph —— 系统符号图标。上一节的 addTabBar 示例里出现过它,这里单独展开:SymbolGlyph 用于显示系统符号库中的矢量图标,与 $r('sys.symbol.xxx') 配套使用。它和位图(Image)的关键区别在于矢量可缩放、颜色可编程fontSize 控制图标大小,fontColor 可传颜色数组实现多色渲染,renderingStrategy 控制渲染策略(SymbolRenderingStrategy.SINGLE 为单色)。本项目底部页签栏就大量使用了这套能力——HomeConstants.FOOTER_TOPIC_ICONS 中存了 house_fillbag_fill_1plusellipsis_message_fillperson_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 声明式范式的核心:

  1. 范式:声明式 UI = 数据 + 绑定 + 描述,状态变化自动刷新,区别于命令式的"逐步绘制";
  2. 骨架@Entry @Component struct + build() 描述组件树;
  3. 布局Navigation(导航容器 + NavPathStack 栈)、Flex(弹性布局)、Column(纵向容器)、Stack(堆叠),属性用链式方法设置;
  4. 组件Text(文本)、Button(按钮 + onClick 事件)、SymbolGlyph(系统符号);
  5. 状态驱动@StorageLink/@StorageProp/@Watch 与 AppStorage 构成全局数据流,replacePath/pushPathByName 实现跳转;
  6. 资源$r('app.xxx') 应用资源、$r('sys.xxx') 系统资源、$rawfile() 原始文件,文案不写死、多语言自动切换。

模块一结语

至此,模块一《项目总览与工程基础》的八篇文章全部完成。回顾整个学习路径:

  • 01 项目总览:建立全局认知,认识六大场景与工程结构;
  • 02 场景演示与运行环境:知道怎么跑、需要什么环境;
  • 03 工程目录全景解析:掌握代码地图与分层哲学;
  • 04 应用配置文件精读:看懂应用与模块配置(skills、continuable、权限);
  • 05 构建体系入门:理解 Hvigor 构建链路与混淆;
  • 06 Stage 模型与 UIAbility 生命周期:读懂入口能力的生命周期与接续机制;
  • 07 ArkTS 语言基础:掌握类、接口、枚举、装饰器;
  • 08 ArkUI 声明式开发范式:掌握声明式 UI、状态驱动与常用组件。

下一步,你可以进入模块二,深入 view/contentEditorview/contentBrowse 两大业务模块的页面实现,看看分布式能力如何在具体页面中落地。

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐