《HarmonyOS技术精讲-UI开发》第1篇:初识ArkTS与声明式UI

《HarmonyOS技术精讲-UI开发》第1篇:初识ArkTS与声明式UI

开篇:为什么需要重新学"写UI"?

如果你刚开始接触 HarmonyOS 开发,第一个要面对的挑战不是 API 有多复杂,而是 ArkTS 这套声明式 UI 写法跟 Android 的 XML 或 iOS 的 Storyboard 完全是两个思路

很多从传统移动端转过来的开发者,第一反应是:"我在代码里写 UI 界面?这不乱套了?"

实际体验下来,ArkTS 的声明式写法在状态管理和 UI 刷新上确实更直接 ,但你得先理解它的核心规则:@Component@Entrybuild() 这三个东西是怎么配合的。

这篇文章的目标很简单:用 Hello World 上手,让你明白 ArkTS 声明式 UI 到底怎么组织代码,并且知道第一版代码里最容易踩哪些坑

环境说明

text 复制代码
DevEco Studio 版本:DevEco Studio 6.1.0 及以上
HarmonyOS SDK 版本:HarmonyOS 6.1.0(23) 及以上
目标设备:手机

本文不涉及版本兼容问题,但建议使用最新稳定版 IDE。

它解决什么问题:用代码"描述"界面,而非"创建"界面

声明式 vs 命令式:一次对比

命令式(传统方式):你一步步告诉程序"创建按钮"、"设置颜色"、"添加到屏幕"。

声明式(ArkTS 方式):你直接描述"界面上有一个按钮,背景为白色,字为黑色",框架自己管怎么渲染和刷新。

特性 命令式 (Android View) 声明式 (ArkTS / SwiftUI)
如何定义 UI 手动创建 + 设置属性 + addView 描述结构 + 绑定状态
状态更新 手动 findView + setText 状态变量变化 → UI 自动刷新
代码结构 多个文件(XML + java) 单文件(结构 + 逻辑)
调试复杂度 中(需关注生命周期) 低(状态驱动)

ArkTS 声明式开发范式 适合的场景:需要频繁更新 UI、页面状态比较复杂、团队人数多的项目。不适合:对渲染性能极度敏感、手写大量 Canvas 绘制的场景(但 ArkUI 对这类需求的性能优化也不错,后续文章会讨论)。

核心概念:组件、装饰器、构建函数

  • @Component:标记一个 struct 为 UI 组件。每个组件就是一个界面部件,可以嵌套。
  • @Entry :标记一个组件为页面入口。没有 @Entry,DevEco Studio 不会把它当作独立页面来运行。
  • build():描述组件长什么样、什么行为。这是声明式 UI 的心脏,所有 UI 代码必须写在这里面。

一句话总结:@Entry 标记页面,@Component 定义组件,build() 画界面。

核心实现:创建一个 Hello World 项目

第一步:新建项目

打开 DevEco Studio,选择"Empty Ability"模板。项目名称随意,包名保持默认。

第二步:编写页面代码

创建完项目后,在 entry/src/main/ets/pages 目录下找到 Index.ets

这是最基础的页面文件结构:

typescript 复制代码
// Index.ets
@Entry
@Component
struct HelloWorldPage {
  build() {
    Column() {
      Text('Hello, HarmonyOS!')
        .fontSize(28)
        .fontWeight(FontWeight.Bold)
        .margin({ bottom: 16 })
      Button('点击我')
        .onClick(() => {
          console.info('按钮被点击')
        })
    }
    .width('100%')
    .height('100%')
    .justifyContent(FlexAlign.Center)
  }
}

代码说明:

  • @Entry 告诉系统这是一个页面入口,可以被路由跳转或作为启动页。
  • @Component 声明一个组件类。
  • struct HelloWorldPage 是组件名,可以自由命名,但建议语义化。
  • build() 内部只能有一个根组件(这里是 Column),这是 ArkUI 的硬性规定。

注意事项:

  • build() 里不能有控制语句(如 if-else)?可以,但要 包裹在 build() 内部。ArkTS 4.0 之后支持条件语句,但仅限于判断组件是否显示,不能用于创建组件实例。
  • 每个组件必须声明 build() 方法,且返回值类型必须是 void
  • ColumnRow 是最常见的布局容器,相当于 HTML 的 flex 布局。

第三步:预览效果

点击 DevEco Studio 右上角的 Previewer 按钮,或者连接真机/模拟器,点击运行。

如果一切正常,你会看到屏幕中央显示"Hello, HarmonyOS!"和一个按钮。

常见问题与避坑(实战经验)

开发 ArkTS 声明式 UI,前几次一定会遇到下面两个问题。

问题 1:@Entry 与 @Component 的顺序写反了

现象 :页面无法被路由找到,预览器报错 @Entry is not a decorator。

原因@Entry 必须写在 @Component 前面,顺序不能颠倒。这不是语法错误,但 ArkUI 的编译期扫描会按顺序解析。如果你不小心写成了:

typescript 复制代码
@Component
@Entry   // 错误:@Entry 必须在前
struct DemoPage {}

IDE 会直接标红,不算隐晦错误,但新手容易忽略。

解决方案 :保持 @Entry 在最上面一行,其次是 @Component

问题 2:build() 方法里不能写业务逻辑

现象 :页面上有一个复杂的列表,你直接在 build() 里循环创建组件,运行后页面卡顿甚至闪退。

原因 :ArkUI 的渲染引擎在每次状态变化时都会重新调用 build() 来重建组件树。如果你在 build() 里做耗时操作(如大量循环、网络请求),UI 线程会被阻塞,导致 ANR(应用无响应)。

解决方案 :把数据准备逻辑放在 aboutToAppear()onPageShow() 中,build() 只负责定义界面。

typescript 复制代码
@Entry
@Component
struct GoodDesign {
  @State dataList: string[] = []

  aboutToAppear() {
    // 在这里做数据初始化,而不是 build() 里
    this.dataList = ['Item1', 'Item2', 'Item3']
  }

  build() {
    List() {
      ForEach(this.dataList, (item: string) => {
        ListItem() {
          Text(item)
        }
      })
    }
  }
}

最佳实践

  1. 每个 @Entry 页面只有一个根组件

    这是 ArkUI 的规则,不要尝试在 build() 里写两个并列的 <Column><Row>,否则编译报错。如果需要并列布局,用 StackFlex 包裹。

  2. 保持良好的命名习惯

    组件名用大写驼峰(HelloWorldPageMainContent),文件命名用小写字母 + 连字符(hello-world-page.ets)。项目混着写太多风格,后面重构会很痛苦。

  3. 避免在 build() 中频繁创建对象

    例如 let colors = ['red', 'blue']; this.colors.forEach(...)。每次 build() 都会创建新对象,ArkUI 的 diff 算法可能认为组件树完全变化,导致无谓重渲染。推荐把数据放到 @State@Observed 中统一管理。

  4. 善用 Previewer 快速验证 UI 逻辑

    Previewer 不需要真机,启动很快,适合快速调样式。但要注意,部分原生 API(如定位、相机)在 Previewer 中会失效,仍然需要真机测试。

Demo 入口

以下是完整项目入口文件:

typescript 复制代码
// entry/src/main/ets/pages/Index.ets
@Entry
@Component
struct Index {
  build() {
    Column() {
      Text('Hello, ArkTS!')
        .fontSize(30)
      Button('点击跳转')
        .onClick(() => {
          router.pushUrl({ url: 'pages/SecondPage' })
        })
    }
  }
}

FAQ(真实开发视角)

Q1:为什么我修改了 @State 变量的值,UI 没有刷新?

A1:最常见原因是你修改的是一个对象属性 ,但 ArkTS 的 @State 只能监听到最顶层引用变化 。比如 @State user = {name: 'Alice'},你只改了 this.user.name = 'Bob',ArkUI 检测不到。解决方案:使用 @Observed 装饰类,或者直接替换整个对象(this.user = {name: 'Bob'})。后续文章会详细讲状态管理。

Q2:页面返回后,之前输入的内容丢了,怎么办?

A2:默认情况下,页面的 @State 变量在页面销毁后会被清除。如果你希望保留状态(如表单数据),需要将状态提升到父组件,或者使用 LocalStorage / AppStorage。对于简单场景,可以先用 @Prop / @Link 从父组件传参。

Q3:为什么 Previewer 显示正常,真机运行就布局变形?

A3:Previewer 的屏幕尺寸和真机差异很大。很多新手喜欢在代码里写 height: '100%',但真机下如果父容器没有明确高度,百分比可能会解析失败。推荐使用 flex 布局或单位 vp / fp,避免依赖百分比。

Q4:build() 方法里能不能写循环或条件?

A4:可以,但要谨慎。ArkTS 4.0 支持 ifForEach,但要注意控制循环体不要有复杂逻辑。官方推荐把复杂条件判断抽到 @State 变量中,然后在 build() 里直接用。

总之,ArkTS 声明式开发范式 上手成本在于理解"声明式"思维,而不是 API 本身。只要记住 @Entry 管页面,@Component 管组件,build() 只管描述界面,第一周就不会走偏。