鸿蒙跨平台开发:Stage 模型 + 响应式布局
一、为什么需要 Stage 模型

早期鸿蒙用 FA 模型开发,每个页面就是一个 Ability,生命周期和窗口管理混在一起。手机场景下够用,但一旦要适配手表、平板、PC,FA 模型的局限就暴露了:窗口大小变化没有统一处理机制,不同设备的生命周期行为不一致,资源调度也缺乏标准。
Stage 模型的核心思路是把应用分成两层:Window 层负责窗口管理(尺寸、多窗口、最大化),Ability 层负责业务逻辑。两层各司其职,Ability 只关心页面内容该怎么渲染,不管窗口怎么变化,跨平台适配因此变得清晰。Ability 的生命周期只描述业务状态,窗口的状态由 Window 层独立维护。
二、断点体系设计
适配多设备的第一步是建立断点体系。ArkUI 不需要设备类型检测,只需要关注一个数字——窗口宽度。设备类型会骗人,比如折叠屏展开 10 英寸、折叠时 6 英寸,但 deviceInfo.deviceType 始终返回 “phone”。真正反映用户实际能看到的空间大小的,是窗口宽度。
业界常用的三级断点足够覆盖主流场景:
- sm(< 600vp):手机竖屏,单栏布局。
- md(600–1200vp):手机横屏或小平板,中等密度网格。
- lg(≥ 1200vp):PC 或大屏设备,侧边栏 + 多列内容。
拿到窗口宽度后,调用一个工具函数就能得到当前断点,代码不过十几行:
type Breakpoint = 'sm' | 'md' | 'lg'
function getBreakpoint(width: number): Breakpoint {
if (width < 600) return 'sm' // 手机
if (width < 1200) return 'md' // 平板
return 'lg' // PC
}
这个函数放在一个工具模块里,全局复用。每次 aboutToAppear 时重新计算,保证窗口 resize 后布局也能正确更新。
三、Ability 入口:根据窗口宽度选择页面
EntryAbility.ets 是应用启动的必经之地,在这里根据窗口宽度决定加载哪个页面。ArkUI 的条件加载能力让这一步变得简单——不需要写多套页面,只需要在入口处做一次路径选择:
import UIAbility from '@ohos.app.ability.UIAbility'
import Window from '@ohos.window'
export default class EntryAbility extends UIAbility {
onWindowStageCreate(windowStage: Window.WindowStage) {
const win = windowStage.getMainWindowSync()
const width = px2vp(win.getWindowProperties().windowWidth)
// 768vp 是经验值,基本能区分手机(竖屏最大约 430vp)和平板横屏
const pagePath = width >= 768 ? 'pages/TwoColumnLayout' : 'pages/Index'
windowStage.loadContent(pagePath, (err) => {
if (err.code) console.error('loadContent failed:', JSON.stringify(err))
})
}
}

768vp 是一个经验值,具体数值可根据产品实际需求调整。如果应用需要支持折叠屏,建议在折叠切换时重新计算断点并触发页面重建。
四、响应式布局的核心结构
页面内部用 @State 保存当前断点,在 aboutToAppear 中获取窗口宽度并计算断点,然后 build() 方法根据断点调用不同的 @Builder 渲染内容。ArkUI 的条件渲染语法是 if / else 分支,而非 CSS 的媒体查询——哪个分支返回就渲染哪个布局,逻辑更直观。
关键在于:布局适配不在 CSS 里,而在 TypeScript 逻辑里。这意味着断点切换时可以配合动画、数据加载等复杂行为,而不只是单纯的样式变化。
@Entry
@Component
struct ResponsiveLayout {
@State bp: Breakpoint = 'sm'
@State windowWidth: number = 360
aboutToAppear() {
const win = Window.getLastWindowSync(getContext(this))
this.windowWidth = px2vp(win.getWindowProperties().windowWidth)
this.bp = getBreakpoint(this.windowWidth)
}
build() {
Column() {
this.TopBar()
if (this.bp === 'sm') this.MobileLayout() else this.DesktopLayout()
}
.width('100%').height('100%').backgroundColor('#F5F5F5')
}
// 顶部设备信息栏,所有断点都显示
@Builder TopBar() {
Row({ space: 10 }) {
Text('📱').fontSize(18)
Text(`设备: ${this.bp} 宽度: ${Math.round(this.windowWidth)}vp`)
.fontSize(13).fontColor('#666666')
Blank()
Text(['📱 手机', '📱 平板', '💻 PC'][this.bp === 'sm' ? 0 : this.bp === 'md' ? 1 : 2])
.fontSize(12).fontColor('#007DFF').padding({ left: 10, right: 10, top: 4, bottom: 4 })
.backgroundColor('rgba(0,125,255,0.1)').borderRadius(12)
}
.width('100%').padding(14).backgroundColor('#FFFFFF')
}
// 手机:单栏垂直堆叠
@Builder MobileLayout() {
Column({ space: 12 }) {
this.Card('🏠 首页', '单栏垂直堆叠,触摸友好,适合手机竖屏。', '#667eea')
this.Card('📝 笔记', '卡片点击展开,全屏显示详情。', '#4facfe')
this.Card('🛒 购物车', '加减数量,实时合计金额。', '#43E97B')
this.FeatureGrid(2)
}.width('100%').padding(16)
}
// PC/平板:双栏分栏
@Builder DesktopLayout() {
Row({ space: 0 }) {
this.SideNav()
Divider().vertical(true).height('100%')
Column({ space: 16 }) {
Row({ space: 12 }) {
this.StatCard('总笔记', '1,284', '#667eea')
this.StatCard('本周新增', '42', '#43E97B')
this.StatCard('协作人数', '8', '#4facfe')
}.width('100%')
this.Card('📊 数据概览', '双栏布局,大屏展示更丰富的信息。', '#f093fb')
this.FeatureGrid(this.bp === 'md' ? 3 : 4)
}.layoutWeight(1).padding(20)
}.width('100%')
}
}

手机布局适合单栏垂直堆叠,触摸交互友好的卡片从顶到底排列,操作按钮放在底部固定区域。每张卡片都是全宽,文字不换行不截断,触控热区大,不容易误操作。
平板布局可以放宽约束,在外层加 maxWidth: 600 限制内容宽度,让内容区居中而不是撑满整个屏幕。网格列数增加到 3 列,信息密度提升但仍保持良好的可读性。
PC 布局用 Row 分成左右两栏,左侧 160vp 固定宽度放导航,右侧 layoutWeight(1) 占满剩余空间放主内容。主内容区进一步用 Row 展示统计数据卡片(宽于普通卡片),网格列数 4 列,充分利用大屏空间。
五、可复用组件
三个 @Builder 组件负责渲染具体内容,可以在任何布局中复用,代码量不大但使用频率最高:
卡片组件最通用,上边加一条彩色装饰线区分不同内容区,内部放标题和描述文字,背景白色加圆角和投影。手机场景下全宽显示,PC 场景下宽度受父容器约束,一套样式自适应。
统计卡片和普通卡片结构一致,区别在于把标题字号加大、数字字号放到 26 加粗,适合展示关键指标。PC 上可以把多个统计卡片排成一行,手机上就垂直堆叠。
网格组件通过 Grid + GridItem 实现,根据传入的列数动态生成列模板。同样一组功能数据,手机传 2 列、平板传 3 列、PC 传 4 列,同样的数据源在不同断点下自动适配密度,无需写三套代码。
@Builder Card(title: string, content: string, color: string) {
Column({ space: 8 }) {
Text(title).fontSize(16).fontWeight(FontWeight.Bold)
Text(content).fontSize(13).fontColor('#666666').lineHeight(20)
}
.width('100%').padding(16).backgroundColor('#FFFFFF').borderRadius(12)
.shadow({ radius: 4, color: 'rgba(0,0,0,0.06)', offsetX: 0, offsetY: 2 })
.border({ top: 3, color: color })
}
@Builder StatCard(label: string, value: string, color: string) {
Column({ space: 6 }) {
Text(label).fontSize(13).fontColor('#888888')
Text(value).fontSize(26).fontWeight(FontWeight.Bold)
}
.width('100%').padding(16).backgroundColor('#FFFFFF').borderRadius(12)
.shadow({ radius: 4, color: 'rgba(0,0,0,0.06)', offsetX: 0, offsetY: 2 })
.border({ top: 3, color: color })
}
@Builder FeatureGrid(cols: number) {
const features = [
{ icon: '📱', name: 'ArkUI' }, { icon: '🔧', name: 'ArkTS' },
{ icon: '🔗', name: '分布式' }, { icon: '🚀', name: 'TaskPool' },
{ icon: '💾', name: '存储' }, { icon: '🌐', name: '网络' },
]
Column({ space: 8 }) {
Text('功能模块').fontSize(14).fontWeight(FontWeight.Bold).width('100%').margin({ left: 4 })
Grid() {
ForEach(features, (f) => {
GridItem() {
Column({ space: 6 }) {
Text(f.icon).fontSize(24)
Text(f.name).fontSize(13)
}.width('100%').height('100%').justifyContent(FlexAlign.Center)
.backgroundColor('#FAFAFA').borderRadius(10)
.onClick(() => console.info(`功能: ${f.name}`))
}
})
}
.columnsTemplate(`1fr `.repeat(cols).trim())
.rowsTemplate('1fr 1fr')
.width('100%').height(160).columnsGap(8).rowsGap(8)
}.width('100%').padding(16).backgroundColor('#FFFFFF').borderRadius(12)
}
六、侧边导航组件
PC 场景下侧边导航栏是标配。和手机底部的 TabBar 不同,侧边导航通常更宽,可以放下图标加文字,还可以加选中态的彩色指示条。导航项的数据结构是数组,用 ForEach 渲染,点击时更新当前选中状态:
@Builder SideNav() {
Column({ space: 4 }) {
ForEach([
{ icon: '🏠', label: '首页', active: true },
{ icon: '📝', label: '笔记', active: false },
{ icon: '🛒', label: '购物', active: false },
{ icon: '⚙️', label: '设置', active: false },
], (item) => {
Row({ space: 10 }) {
Text(item.icon).fontSize(18)
Text(item.label).fontSize(14).fontColor(item.active ? '#007DFF' : '#666666')
if (item.active) {
Blank()
Text('').width(3).height(20).backgroundColor('#007DFF').borderRadius(2)
}
}
.width('100%').height(48).padding({ left: 16, right: 16 }).borderRadius(8)
.backgroundColor(item.active ? 'rgba(0,125,255,0.08)' : 'transparent')
.onClick(() => console.info(`导航: ${item.label}`))
})
}.width(160).height('100%').padding(12).backgroundColor('#FFFFFF')
}
七、PC 专属交互
PC 上的交互比手机丰富得多。鼠标悬停(Hover)是 PC 独有的——鼠标移入时触发状态变化,可以做颜色加深、阴影增强等视觉反馈,比手机触摸更早一步给用户暗示。ArkUI 通过 onHover 回调处理,传入布尔值区分进入和离开,配合 animateTo 可以做平滑的过渡动画:
@State hoverText: string = '将鼠标移入蓝色区域'
Column()
.width(200).height(80).backgroundColor('#667eea').borderRadius(12)
.onHover((isHover: boolean) => {
animateTo({ duration: 200 }, () => {
this.hoverText = isHover ? '✅ 鼠标已悬停' : '将鼠标移入蓝色区域'
})
})
.child(Text(this.hoverText).fontSize(13).fontColor('#FFFFFF')
.width('100%').textAlign(TextAlign.Center))
.alignItems(HorizontalAlign.Center).justifyContent(FlexAlign.Center)
键盘快捷键是 PC 效率工具的核心。Ctrl+S 保存、Ctrl+N 新建、Ctrl+F 搜索、Escape 关闭——这些约定俗成的快捷键可以显著提升 PC 用户的操作效率,在表单类应用中尤其重要。ArkUI 目前通过监听键盘事件处理,后续版本会提供更完整的快捷键注册机制。
八、窗口监听与动态适配
PC 应用需要实时响应窗口大小的改变。通过 window.on('windowSizeChange') 可以监听窗口变化,在回调里重新计算断点并更新状态变量,布局会自动重建:
aboutToAppear() {
const win = Window.getLastWindowSync(getContext(this))
// 注册窗口变化监听
win.on('windowSizeChange', (size) => {
this.windowWidth = px2vp(size.width)
this.bp = getBreakpoint(this.windowWidth)
})
}
aboutToDisappear() {
// 取消监听,防止内存泄漏
const win = Window.getLastWindowSync(getContext(this))
win.off('windowSizeChange')
}
这里有一个常见的坑:监听注册后没有取消。每次页面重建时 aboutToAppear 都会被调用,如果不在 aboutToDisappear 中清理,监听器会越积越多,内存占用持续增长,直到应用崩溃。
九、实战建议
适配的核心原则是从窗口宽度出发,而非设备类型。windowWidth >= 768 这种判断直接反映用户实际能看到的空间大小,而非设备标称形态。设备类型会骗人,但窗口宽度不会。
组件设计应遵循最大公约数原则:优先使用所有平台都支持的交互(点击、长按),PC 特有的 Hover 和右键菜单用条件分支增强,不确定时优先用点击而非手势。断点变化时用 animateTo 包裹布局属性过渡,避免内容闪烁。
页面路径选择放在 Ability 入口层,组件内部只做布局适配——这个分层原则值得坚持。页面路径是架构决策,布局适配是表现层细节,两者在不同层级处理,代码更清晰,维护成本也更低。
更多推荐



所有评论(0)