一、为什么需要 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 入口层,组件内部只做布局适配——这个分层原则值得坚持。页面路径是架构决策,布局适配是表现层细节,两者在不同层级处理,代码更清晰,维护成本也更低。


Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐