CHAPTER 02 · 10

ArkUI 声明式 UI

掌握 ArkUI 核心装饰器体系、布局容器、内置组件,理解声明式 UI 的渲染机制与组件生命周期。

声明式 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() 中。