ArkUI 的声明式 UI 心智模型
ArkUI 是鸿蒙的界面框架,核心是“状态驱动视图”。你的代码只描述界面结构,框架负责在状态变化时重新计算差异并更新真实渲染节点。
这套模型带来一个重要的思维转变:不要去想“怎么改这个控件”,而要想“什么状态变了、界面应该长什么样”。一旦习惯了后者,代码会变得更短、更可预测,也更容易测试。
声明式 UI 里,界面是状态的函数:UI = f(State)。所有复杂问题,都该在“状态怎么组织”这一层解决,而不是在“怎么改视图”那一层打补丁。
因此 ArkUI 开发的核心能力其实是两件事:把界面拆成合理的组件树,把状态放在正确的层级。布局只是这两件事的表达方式。
布局容器:Column、Row、Stack 与 Flex
ArkUI 提供了几种基础容器,掌握它们的适用场景比记住参数更重要。
- Column / Row纵向与横向线性排列,最常用;用 space 控制间距,用 justifyContent 控制主轴对齐。
- Stack层叠排列,后加入的在上层;适合角标、遮罩、图文叠加。
- Flex更灵活的弹性布局,支持换行与自定义权重,是复杂排版的兜底方案。
- RelativeContainer相对定位,适合需要明确锚点关系的复杂面板。
布局的关键属性是 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。
实战:一个响应式商品卡片列表
把容器、组件、列表与状态串起来,做一个根据屏幕宽度自动调整列数的商品列表。
@Entry
@Component
struct ProductListPage {
@State products: Product[] = [
{ id: 1, name: '鸿蒙开发实战', price: 89.0, cover: $r('app.media.cover1') },
{ id: 2, name: 'ArkTS 语言精讲', price: 69.0, cover: $r('app.media.cover2') },
];
@State columns: number = 2;
aboutToAppear(): void {
// 实际项目用窗口尺寸变化回调替换此处的一次性估算
this.columns = display.getDefaultDisplaySync().width > 900 ? 4 : 2;
}
build() {
Column() {
Text('推荐商品').fontSize(20).fontWeight(FontWeight.Bold)
.width('100%').padding({ left: 16, top: 16, bottom: 8 })
Grid() {
ForEach(this.products, (p: Product) => {
GridItem() {
ProductCard({ name: p.name, price: p.price, cover: p.cover })
}
}, (p: Product) => p.id.toString())
}
.columnsTemplate(`repeat(${this.columns}, 1fr)`)
.columnsGap(12)
.rowsGap(12)
.padding({ left: 16, right: 16 })
}
.width('100%').height('100%')
.backgroundColor('#F5F7FA')
}
}
核心是 columnsTemplate 与状态绑定:列数变了,模板字符串变了,界面自动重排。真实项目里应监听窗口尺寸变化事件动态更新 columns,而不是只在 aboutToAppear 里算一次——折叠屏展开时列数需要实时变化。
性能与调试:布局层级与渲染耗时
界面卡顿通常来自三处:布局层级过深、列表项过于复杂、以及不必要的重复渲染。
- 控制嵌套深度:尽量用扁平结构,减少无意义的容器包裹;一个 Row 套 Column 只为了设置间距,就是多余层级。
- 列表项保持轻量:列表项里避免放重量级组件与复杂计算,能把结果预先算好就预先算好。
- 缩小状态范围:把频繁变化的状态放在最小粒度的组件里,避免一个状态的更新触发整页重算。
- 图片指定合适尺寸:不要用大图缩略显示,既费内存又费解码时间。
在移动端,一次“整页重算”的代价远高于十次“局部更新”。状态的组织方式,直接决定了界面流畅度。
排查手段上,DevEco Studio 提供了布局边界显示与渲染耗时分析能力,能直观看到哪里有过度绘制。把界面调顺之后,下一步可以读 Stage 模型与生命周期,理解页面在应用架构中的位置。