HarmonyOS用 ArkTS 描述界面,让数据驱动渲染

声明式到底"声明"了什么

命令式编程里,你要告诉程序"怎么做"——先创建按钮,再设置它的位置,再绑定点击回调。每一步都是具体的操作指令。

声明式编程里,你只告诉程序"要什么"——这里有个按钮,文字是"登录",点它触发这个动作。至于按钮怎么绘制、怎么布局、怎么响应,统统交给框架。

用一个计数器的例子对比,差异一目了然:

// 命令式思路(伪代码,鸿蒙不这么写)
let count = 0;
let text = createText(count.toString());
let btn = createButton("+1");
btn.onClick(() => {
  count++;
  text.setText(count.toString());  // 手动更新 UI
});
// 声明式思路(ArkTS)
@Entry
@Component
struct Counter {
  @State count: number = 0;  // 声明一个状态变量

  build() {
    Column() {
      Text(this.count.toString())  // UI 直接引用状态
        .fontSize(40)
      Button('+1')
        .onClick(() => {
          this.count++;  // 只改数据,不改 UI
        })
    }
  }
}

关键区别:声明式里 count++ 之后你没有碰任何 UI 代码,但界面会自动更新。这就是"数据驱动 UI",开发者只写引起界面变化的数据,界面怎么变交给框架。

ArkTS 的三个扩展

ArkTS 在 TypeScript 基础上做了扩展,扩展方向正好对应声明式 UI 的核心需求:

扩展作用典型语法
声明式 UI 描述用接近自然语言的方式描述界面结构Column { Text(...) }
自定义组件把界面拆成可复用的独立单元@Component struct MyComp {}
动态扩展 UI 元素运行时动态增删界面元素@BuilderParamsif/forEach
状态管理声明数据与 UI 的绑定关系@State @Prop @Link
渲染控制根据条件/循环控制渲染if ForEach LazyForEach

最小可运行结构

一个声明式页面,最小结构长这样:

// 页面入口
@Entry              // 标记为页面入口,可被路由加载
@Component           // 标记为自定义组件
struct HomePage {
  build() {          // 唯一入口,描述 UI 结构
    Text('Hello ArkUI')
  }
}

@Entry@Component 是两个装饰器:

  • @Entry:表示这是一个页面,可以独立加载
  • @Component:表示这是一个自定义组件

build() 是组件的唯一渲染入口,里面写 UI 结构。

UI 结构的组织

声明式 UI 通过组件嵌套组织。容器组件(如 Column)包住子组件,形成树状结构:

build() {
  Column {              // 容器:纵向排列
    Text('标题')
      .fontSize(24)     // 链式调用设置属性
      .fontWeight(FontWeight.Bold)
    Row {               // 容器:横向排列
      Button('确定')
      Button('取消')
    }
  }
  .padding(16)          // 容器自身的属性
  .width('100%')
}

组件属性用链式调用设置,一个组件一行或几行,读起来接近自然语义。

状态管理让 UI 活起来

静态结构写好了,要让界面"活",靠状态管理。最常用的是 @State

@Entry
@Component
struct LoginPage {
  @State username: string = '';  // 输入框内容
  @State remember: boolean = false;  // 是否记住密码

  build() {
    Column({ space: 12 }) {
      TextInput({ placeholder: '请输入用户名' })
        .onChange((value: string) => {
          this.username = value;  // 输入变化,同步到状态
        })

      Text(`当前输入:${this.username}`)  // 引用状态,自动刷新

      Row() {
        Checkbox()
          .select(this.remember)
          .onChange((checked: boolean) => {
            this.remember = checked;
          })
        Text('记住密码')
      }

      Button('登录')
        .enabled(this.username.length > 0)  // 状态驱动可用性
        .onClick(() => {
          this.doLogin();
        })
    }
    .padding(16)
  }

  doLogin(): void {
    // 登录逻辑
  }
}

这个例子里,username 变化会自动刷新两个地方:提示文本和登录按钮的可用状态。你只改了数据,框架自动定位到引用它的组件做更新。

渲染控制:条件与循环

列表和条件渲染是界面开发的高频需求,ArkTS 有专门语法:

@Entry
@Component
struct TaskList {
  @State tasks: Task[] = [];  // 任务列表

  build() {
    Column() {
      // 条件渲染
      if (this.tasks.length === 0) {
        Text('暂无任务')  // 空态
      } else {
        // 循环渲染
        ForEach(this.tasks, (task: Task) => {
          TaskItem({ task: task })
        }, (task: Task) => task.id.toString())  // 第三个参数是 key,必须唯一
      }
    }
  }
}

@Component
struct TaskItem {
  task: Task;  // 父组件传入的数据

  build() {
    Row() {
      Text(this.task.title)
      Button('完成')
    }
  }
}

两个要点:

  • ForEach 的第三个参数(key 生成器)必须提供且返回唯一值,否则列表更新会错乱
  • 大数据量用 LazyForEach 替代 ForEach,只渲染可见项,性能更好

技术架构落到代码上

之前讲的后端引擎 C++ 架构,在代码层的体现是:你写的声明式描述,会被编译和运行时转换成对 C++ 组件的调用。开发时不用关心这个转换,但要知道两点:

  1. UI 前端和后端分层:前端是你写的 ArkTS 结构,后端是 C++ 组件和渲染管线。数据在两边间传递,状态变化会触发后端重新计算。

  2. 编译器优化:统一字节码、AOT 编译、高效 FFI(跨语言调用)、引擎极小化。这些让 ArkTS 代码即使有跨语言调用,性能也能保证。

状态的两个层次

状态管理分两层,配合使用能构建完整应用:

应用级状态          AppStorage / LocalStorage / PersistentStorage
(跨页面共享)        ↑ 全局数据、持久化配置
─────────────────────────────
组件级状态          @State / @Prop / @Link / @Watch
(组件内部/父子间)     ↑ 单个组件的 UI 状态

先用组件级状态把单页逻辑写对,再用应用级状态处理跨页共享,这是清晰的状态分层思路。具体每个装饰器的用法,后面状态管理专题展开。

一条容易忽略的通则

声明式范式里,长度数字默认单位是 vp(虚拟像素)。写 .width(200) 就是 200vp,会自动适配不同屏幕密度。如果用 px 需要显式写 .width('200px')

另一个注意点是异常值处理:传 undefinednull 或无效值时,有默认值的参数走默认值,没有默认值的参数直接不生效。这意味着不会因为传错值而崩溃,但也可能让你误以为设置成功了——调试时留意属性是否真的生效。

小小建议

  1. 先写结构再写状态。把 build() 里的静态结构搭好,跑起来看到界面,再逐步加 @State 让界面动起来。一步到位容易乱。

  2. 链式调用分行写。一个属性一行,别挤在一行里,可读性差很多,团队协作尤其明显。

  3. ForEach 的 key 别省略。省略 key 列表更新会出 bug,这是列表开发最容易出问题的地方。

  4. 容器嵌套别太深ColumnRowColumn 套三层以上就该拆自定义组件了,深嵌套既难读也影响性能。

Logo

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

更多推荐