1. 声明式 UI 的基本思路

ArkUI 的核心思想是声明式 UI:开发者不再一步步命令界面“创建按钮、修改文字、隐藏列表”,而是描述在某个状态下界面应该长什么样。当状态变化时,框架负责找到相关 UI 片段并刷新。这个思想听起来简单,但真正写项目时,很多问题都来自状态边界不清楚:哪些数据应该响应式,哪些只是普通变量,父子组件之间应该单向传递还是双向绑定,列表更新为什么没有刷新,页面为什么刷新太多。

2. ArkTS 与声明式开发的关系

ArkTS 是鸿蒙应用的主力开发语言,它保留了 TypeScript 的很多语法习惯,同时加强了静态检查和类型约束。官方资料强调 ArkTS 通过更严格的规范减少运行时错误,并帮助编译器做更深入优化。对开发者来说,这意味着不能完全按 JavaScript 那种“运行时想怎么改对象就怎么改”的方式写代码。对象结构、字段类型、组件状态都应该更明确,这也正好契合声明式 UI 的开发方式。

图 5  ArkUI 声明式组件树

3. build 函数只描述界面

在 ArkUI 中,组件通常由 struct、build 函数和状态变量组成。build 不是一个适合放复杂业务逻辑的地方,它更像 UI 描述函数。你可以在里面写 Column、Row、Text、Button、List 等组件组合,但不应该在里面发网络请求、做大循环计算或频繁创建重量对象。build 可能因为状态变化被重新执行,放入副作用逻辑会造成重复请求、性能波动和难以排查的状态错乱。

4. @State、@Prop 与 @Link 怎么选

@State 是最常见的状态装饰器,表示组件内部拥有并维护这份状态。它适合按钮计数、当前 Tab、输入框内容、列表筛选条件等与组件视图强相关的数据。@Prop 更像父组件传给子组件的只读输入,子组件拿到的是一份值,适合展示标题、数量、配置项。@Link 表示父子之间双向绑定,子组件修改后父组件也会感知,适合表单、开关、编辑器等需要子组件反向写入的场景。

5. @Provide 和 @Consume 的使用边界

当组件层级变深时,如果每一层都通过参数向下传,代码会变得很啰嗦。@Provide 和 @Consume 用来跨层级传递共享状态,适合主题、语言、登录态、页面上下文等相对稳定的全局或局部上下文。但它不是让你把所有数据都塞进去的万能仓库。状态共享越方便,越容易让依赖关系变隐蔽;如果一个深层组件突然改了全局状态,页面其他地方跟着刷新,排查成本会很高。

图 6  常见状态装饰器关系

6. 复杂对象状态怎么观察

复杂对象状态需要更谨慎。很多初学者会写一个普通对象放进 @State,然后修改对象内部字段,却发现界面没有按预期刷新。原因在于响应式系统需要能观察到变化边界。对于对象模型,可以结合 @Observed、@ObjectLink 等机制,让对象属性变化被框架感知;也可以采用不可变更新思路,每次生成新对象或新数组引用。两种方式没有绝对优劣,关键是团队要统一写法。

7. 长列表为什么要特别设计

列表是 ArkUI 性能问题最常见的来源。少量静态列表用 ForEach 问题不大,但长列表、分页列表、瀑布流或聊天记录,更应考虑 LazyForEach 等懒加载方式。长列表不只是渲染数量多,还会带来图片加载、布局测量、滚动回收、状态保持等问题。列表项组件要尽量轻,图片和格式化逻辑要复用,点击态、选中态、展开态要有清晰 key,否则一刷新就可能出现错位或状态丢失。

图 7  状态变化到界面刷新的链路

8. 组件拆分要看刷新边界

声明式 UI 的性能优化,不是把所有组件写在一个文件里减少层级。相反,合理拆分组件通常更有利于控制刷新范围。比如订单列表页面可以拆成筛选栏、统计栏、订单卡片、空状态、加载状态;筛选条件变化时不应让无关的顶部用户信息重建,某个订单状态变化时也不应重建整个页面。组件拆分的依据不是视觉分块,而是状态依赖和刷新边界。

9. 样式和资源也要工程化

样式和资源也会影响维护成本。ArkUI 组件链式调用很方便,但如果每个页面都复制一堆 fontSize、fontColor、padding、borderRadius,后期改设计规范会很痛苦。可以把通用样式抽成函数、常量或基础组件,例如统一的标题栏、信息卡片、按钮行、状态标签。这样不仅减少重复,也能让页面代码更专注于业务结构。

图 8  ArkUI 性能写法检查表

10. 按数据来源拆状态

实际写页面时,可以先按数据来源拆状态。接口返回的远端数据、页面临时交互状态、组件内部动画状态、应用级配置状态,最好不要混在同一个对象里。远端数据适合由页面模型或数据层管理,页面临时状态适合 @State,子组件编辑态可以用 @Link,跨页面配置再考虑应用级存储。这样做的好处是每个状态为什么存在、谁能修改、变化后影响哪里,都能讲得清楚。

11. 表单页面的状态组织

表单页面尤其考验状态设计。一个注册页面可能包含手机号、验证码、密码、协议勾选、按钮可用状态、错误提示、倒计时等数据。如果全部塞进一个大对象,任何字段变化都可能牵动整块 UI;如果全部拆成零散变量,又会让校验逻辑分散。比较实用的写法是把输入值和错误信息分组管理,把按钮可用状态写成派生逻辑,把倒计时这种独立变化的状态拆到验证码组件内部。

12. 刷新问题的三步排查

调试刷新问题时,可以按三步走。第一步确认事件确实触发,例如 onClick、onChange 是否执行;第二步确认修改的是响应式状态,而不是普通局部变量或未观察到的对象内部字段;第三步缩小刷新范围,观察是没有刷新、刷新太多,还是列表项复用导致显示错位。这个排查顺序很朴素,但能解决大部分 ArkUI 初学阶段遇到的页面问题。

13. 团队 ArkUI 编码规范

团队落地 ArkUI 时,还可以约定几条编码规范。页面组件只负责组合布局和状态分发,复杂业务放到模型层;列表项组件必须有稳定标识,不能依赖数组下标表达业务身份;通用组件只暴露必要参数,不把父页面状态整包传入;异步请求完成后先更新数据模型,再由状态驱动界面变化。规范看起来普通,却能明显减少后期因为状态来源不明、组件耦合过深而产生的维护成本。

14. 本文小结

最后要强调调试思路。遇到 UI 不刷新,先看状态是否被正确声明,修改是否发生在可观察边界,数组或对象是否只是内部字段变化;遇到刷新过多,先看状态是否放得太高,子组件是否依赖了不必要的父状态,build 中是否创建了大量临时对象。ArkUI 的声明式开发并不神秘,它要求开发者把数据流、组件树和刷新边界想清楚。能做到这一点,页面代码会更短、更稳,也更接近鸿蒙生态推荐的开发方式。

15. 状态装饰器选择表

装饰器

数据关系

适合场景

@State

组件自己拥有,变化触发自身刷新

局部输入、Tab、筛选条件

@Prop

父到子单向传值

展示型子组件、标题、配置

@Link

父子双向同步

表单编辑、开关、选择器

@Provide/@Consume

跨层级共享

主题、语言、页面上下文

@Observed/@ObjectLink

对象属性可观察

复杂模型、列表项对象

16. 案例代码:@State 驱动局部 UI

@Component

struct CounterPanel {

  @State count: number = 0;

  build() {

    Column() {

      Text(`当前数量:${this.count}`)

      Button('加一').onClick(() => { this.count++; })

    }

  }

}

@State 的关键是“组件自己拥有这份状态”。上面的 count 只影响 CounterPanel 的展示,不需要放到全局,也不需要父组件管理。局部状态越局部,刷新范围越容易控制,页面也越不容易出现无关重建。

17. 案例代码:@Prop 与 @Link 的区别

@Component

struct ChildPanel {

  @Prop title: string;

  @Link enabled: boolean;

  build() {

    Row() {

      Text(this.title)

      Toggle({ type: ToggleType.Switch, isOn: this.enabled })

        .onChange((value: boolean) => { this.enabled = value; })

    }

  }

}

title 只是父组件传进来的展示文本,子组件不应该反向修改,所以用 @Prop;enabled 是开关状态,用户在子组件里点击后要同步回父组件,所以用 @Link。很多状态混乱都来自把单向数据写成双向,或者把双向编辑误写成普通参数。

18. 案例代码:@Provide 与 @Consume 跨层级共享主题

@Component

struct RootPage {

  @Provide themeColor: Color = Color.Blue;

  build() {

    Column() {

      Toolbar()

      ContentArea()

    }

  }

}

@Component

struct Toolbar {

  @Consume themeColor: Color;

  build() { Text('标题').fontColor(this.themeColor) }

}

@Provide/@Consume 适合主题色、语言、页面上下文这类跨层级信息。它能减少逐层传参,但也会让依赖关系变隐蔽,所以不要把所有业务数据都放进去。经验上,越频繁变化、越具体的业务状态,越不适合做跨层级共享。

19. 案例代码:@Observed 与 @ObjectLink 管理对象属性

@Observed

class TodoItem {

  title: string;

  done: boolean;

  constructor(title: string, done: boolean) {

    this.title = title;

    this.done = done;

  }

}

@Component

struct TodoRow {

  @ObjectLink item: TodoItem;

  build() {

    Row() {

      Checkbox().select(this.item.done)

        .onChange((checked: boolean) => { this.item.done = checked; })

      Text(this.item.title)

    }

  }

}

当列表项是对象时,只修改 item.done 这种内部字段,普通状态不一定能被正确观察。用 @Observed 标记模型、用 @ObjectLink 传给子组件,可以让对象属性变化更贴近 UI 刷新。适合待办项、购物车商品、设备状态卡片等对象型数据。

20. 案例代码:长列表使用 LazyForEach 思路

@Component

struct ProductListPage {

  private dataSource: ProductDataSource = new ProductDataSource();

  build() {

    List() {

      LazyForEach(this.dataSource, (item: Product) => {

        ListItem() {

          ProductCard({ product: item })

        }

      }, (item: Product) => item.id)

    }

  }

}

长列表要关注两个点:一是不要一次性创建所有列表项,二是 key 必须稳定。稳定 key 能帮助框架识别列表项身份,避免滚动、刷新、插入数据时出现状态错位。商品列表、聊天记录、文件列表都应尽早设计数据源和 key。

参考资料

  • 华为开发者文档中心:https://developer.huawei.com/consumer/cn/doc/
  • HarmonyOS Stage 模型文档:https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/process-model-stage
  • ArkUI 官方介绍:https://developer.huawei.com/consumer/cn/arkui/
  • ArkTS 官方介绍:https://developer.huawei.com/consumer/en/arkts/
  • ArkUI 状态管理相关文档:https://developer.huawei.com/consumer/cn/doc/harmonyos-guides-v5/arkts-new-binding-V5
Logo

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

更多推荐