ArkUI 的声明式 UI 心智模型

ArkUI 是鸿蒙的界面框架,核心是“状态驱动视图”。你的代码只描述界面结构,框架负责在状态变化时重新计算差异并更新真实渲染节点。

这套模型带来一个重要的思维转变:不要去想“怎么改这个控件”,而要想“什么状态变了、界面应该长什么样”。一旦习惯了后者,代码会变得更短、更可预测,也更容易测试。

声明式 UI 里,界面是状态的函数:UI = f(State)。所有复杂问题,都该在“状态怎么组织”这一层解决,而不是在“怎么改视图”那一层打补丁。

因此 ArkUI 开发的核心能力其实是两件事:把界面拆成合理的组件树,把状态放在正确的层级。布局只是这两件事的表达方式。

布局容器:Column、Row、Stack 与 Flex

ArkUI 提供了几种基础容器,掌握它们的适用场景比记住参数更重要。

四种容器与适用场景
  • Column / Row纵向与横向线性排列,最常用;用 space 控制间距,用 justifyContent 控制主轴对齐。
  • Stack层叠排列,后加入的在上层;适合角标、遮罩、图文叠加。
  • Flex更灵活的弹性布局,支持换行与自定义权重,是复杂排版的兜底方案。
  • RelativeContainer相对定位,适合需要明确锚点关系的复杂面板。
ArkUI 示意:多张半透明圆角组件卡片错落层叠,弧形发光箭头在卡片之间流转
组件与状态流转:界面由可复用的组件拼装,状态变化沿着箭头自动传播。

布局的关键属性是 layoutWeight。给子组件设置 layoutWeight(1),它就按权重瓜分剩余空间——这是实现“输入框自适应、按钮固定宽”这类布局最干净的办法,比手工算宽度可靠得多。

常用组件与链式属性

ArkUI 的内置组件覆盖了绝大多数界面需求:Text、Image、Button、TextInput、Checkbox、Toggle、Slider、Progress、List、Grid、Tabs、Swiper、Dialog 等。它们的属性都通过链式调用设置,可读性很好。

@Component
struct ProductCard {
  @Prop name: string;
  @Prop price: number;
  @Prop cover: Resource;

  build() {
    Column({ space: 8 }) {
      Image(this.cover)
        .width('100%')
        .aspectRatio(16 / 9)
        .objectFit(ImageFit.Cover)
        .borderRadius(12)
      Row({ space: 6 }) {
        Text(this.name).fontSize(16).fontWeight(FontWeight.Medium).layoutWeight(1)
        Text(`¥${this.price.toFixed(2)}`).fontSize(16).fontColor('#1E5FA8')
      }.width('100%')
    }
    .padding(12)
    .backgroundColor(Color.White)
    .borderRadius(16)
    .shadow({ radius: 12, color: '#1A1E5FA8', offsetY: 4 })
  }
}

这段代码展示了三个实用技巧:用 aspectRatio 固定图片比例避免布局跳动;用 layoutWeight 让标题占满剩余宽度、价格自然右对齐;用 shadow 与 borderRadius 组合出卡片质感。把这样的组件抽出来,列表页面只需要一行调用。

列表与懒加载:List 与 LazyForEach

列表是移动端最重要的组件,也是最容易出性能问题的地方。ArkUI 的 List 支持滚动懒渲染,但前提是你用对了循环方式。

  • 数据量小(几十条以内):用 ForEach,写法简单。
  • 数据量大或不确定:用 LazyForEach 配合 IDataSource,只创建可见区域的列表项。
class ProductSource implements IDataSource {
  private data: Product[] = [];
  private listeners: DataChangeListener[] = [];

  totalCount(): number { return this.data.length; }
  getData(index: number): Product { return this.data[index]; }

  registerDataChangeListener(l: DataChangeListener): void { this.listeners.push(l); }
  unregisterDataChangeListener(l: DataChangeListener): void {
    const i = this.listeners.indexOf(l);
    if (i >= 0) { this.listeners.splice(i, 1); }
  }

  push(item: Product): void {
    this.data.push(item);
    this.listeners.forEach(l => l.onDataAdd(this.data.length - 1));
  }
}

关键点是数据变化必须通过监听器通知:onDataAdd、onDataChange、onDataDelete 分别对应增、改、删。只在集合里改数据而不通知,界面不会更新——这是懒加载列表最常见的“数据变了界面不动”的原因。

状态管理进阶:@Provide / @Consume 与 AppStorage

当组件层级很深,逐层用 @Prop / @Link 传递参数会非常啰嗦。ArkUI 提供了跨层级的状态共享机制。

@Provide 与 @Consume 是一对:祖先组件用 @Provide 声明数据,任意深度的后代用 @Consume 直接消费,中间层完全不需要中转。适合主题色、登录状态、语言这类全页面共享的数据。

AppStorage 则是应用级单例存储:任何组件都能读写,配合 @StorageProp(单向)与 @StorageLink(双向)绑定到组件。适合用户信息、设置项这类跨页面共享的状态。但要克制使用——全局状态越多,数据流越难追踪,出问题时定位成本急剧上升。

一条经验法则:能用父子传参解决的,就不要用全局状态。全局状态只留给真正跨页面的少数数据。

响应式适配:单位、断点与栅格

鸿蒙设备形态多:手机、折叠屏、平板、智慧屏。适配的关键是不要写死像素。

  • 尺寸单位用 vp(虚拟像素,自动按屏幕密度换算),字体用 fp(会跟随系统字号设置缩放)。
  • 百分比与 layoutWeight 优先于固定宽高,让布局随容器自适应。
  • 用断点(BreakpointReference 或媒体查询)区分 sm / md / lg,针对平板调整栏数与间距。
  • 多列场景用 GridRow / GridCol 栅格,一行 12 栅,按断点指定 span,改动成本最低。

另一个易忽略点是安全区域:折叠屏与曲面屏需要给内容留出避让边距,使用系统提供的安全区域接口,而不是拍脑袋写 24vp 的 padding。