这篇不从“折叠屏有几种形态”开始,而是从一个更具体的问题切入:同一个笔记页面,在 420vp 的窄窗口里应该是单栏,在 840vp 的展开窗口里应该变成列表 + 详情双栏;真正麻烦的是,布局切换以后,用户正在编辑的草稿不能丢,当前选中的笔记也不能跳回首页。

我给 Demo 起名叫 FoldNoteDesk。这次固定一组运行状态,方便正文、日志和图片保持一致:

  • Window:420vp → 840vp
  • Mode:STACK → SPLIT
  • Note ID:note_20261001_07
  • Draft:DIRTY
  • Unsaved:126 chars
  • Breakpoint:COMPACT → EXPANDED

官方当前的多设备适配指南强调,折叠屏在展开、折叠以及窗口尺寸变化过程中,应用需要保证布局适配和状态连续;ArkUI 的 Navigation 也提供单栏、分栏和自适应模式。对我这个笔记场景来说,真正值得处理的是“窗口变化驱动布局”,而不是只判断设备是不是折叠屏。

一、我没有直接监听折叠状态,而是先监听窗口宽度

最早我也想过直接拿折叠状态做判断:

展开就双栏,折叠就单栏。

但实际项目里很快就会遇到自由窗口、分屏、横竖屏这些情况。设备即使处在展开态,应用窗口本身也可能很窄;反过来,某些宽屏设备根本不是折叠屏,也同样适合双栏。

所以最后我把布局判断依据改成了窗口宽度。

这段代码解决的问题,是在应用启动和后续窗口变化时,持续得到当前可用窗口宽度,并把结果写进全局断点状态。

import { UIAbility } from '@kit.AbilityKit';
import { display, window } from '@kit.ArkUI';

export default class EntryAbility extends UIAbility {
  onWindowStageCreate(windowStage: window.WindowStage): void {
    windowStage.getMainWindow().then((windowObj) => {
      this.updateBreakpoint(
        windowObj.getWindowProperties().windowRect.width
      );

      windowObj.on('windowSizeChange', (size) => {
        this.updateBreakpoint(size.width);
      });
    });
  }

  private updateBreakpoint(widthPx: number): void {
    const density =
      display.getDefaultDisplaySync().densityPixels;
    const widthVp = widthPx / density;

    const breakpoint =
      widthVp >= 600 ? 'EXPANDED' : 'COMPACT';

    AppStorage.setOrCreate(
      'windowBreakpoint',
      breakpoint
    );

    AppStorage.setOrCreate(
      'windowWidthVp',
      Math.round(widthVp)
    );
  }
}

这里的 600vp 是当前 Demo 的业务断点,不是系统强制值。

它的作用只是表达:我的笔记应用在小于 600vp 时,列表和详情挤在一起会影响编辑;超过这个宽度以后,双栏开始有实际收益。

实际项目里,断点应该根据内容密度、字号、导航结构重新验收,而不是所有页面共用一个数字。

官方开发资料也推荐在响应式布局里根据窗口宽度划分断点,并通过 windowSizeChange 处理动态变化。这个思路比“检测设备型号”更适合长期维护。

二、布局切换不能顺手把业务状态也重建

做到这里以后,我遇到第二个问题。

最初页面里写的是:

if (this.breakpoint === 'EXPANDED') {
  this.buildSplitPage()
} else {
  this.buildStackPage()
}

看上去没问题,但列表、详情、编辑器实际上被当成了两套 UI 树。

一旦状态绑定做得不干净,窗口一变化,就可能出现:

  • 详情页被重新创建;
  • 当前 Note ID 回到默认值;
  • 编辑中的 TextArea 重新取服务器旧内容;
  • 草稿 DIRTY 状态被重置。

所以我后面改成:布局只决定 Navigation 的显示模式,不决定当前业务数据是谁。

这段代码解决的是“同一份选中状态,在单双栏之间复用”。

@Entry
@Component
struct WorkspacePage {
  private navPathStack: NavPathStack = new NavPathStack();

  @StorageLink('windowBreakpoint')
  breakpoint: string = 'COMPACT';

  @StorageLink('selectedNoteId')
  selectedNoteId: string = 'note_20261001_07';

  @StorageLink('draftContent')
  draftContent: string = '';

  private getNavigationMode(): NavigationMode {
    return this.breakpoint === 'EXPANDED'
      ? NavigationMode.Split
      : NavigationMode.Stack;
  }

  build() {
    Navigation(this.navPathStack) {
      NoteList({
        selectedNoteId: this.selectedNoteId
      })
    }
    .mode(this.getNavigationMode())
  }
}

这一层最重要的变化,是 selectedNoteId 和 draftContent 不再属于某个“单栏组件”或“双栏组件”。

它们属于 Workspace 业务本身。

页面从 420vp 切到 840vp,UI 可以重排,数据不能因为重排就换一份。

三、草稿状态我单独做了一层,不让 TextArea 成为唯一真相

接下来是最容易被忽略的一点。

如果用户正在输入 126 个未保存字符,此时设备展开,窗口触发重排,TextArea 组件自己的内部状态并不应该承担“草稿持久化”责任。

所以我给 FoldNoteDesk 加了一个很薄的 DraftStore。

当前代码解决的是:输入变化以后立刻更新业务草稿,同时标记当前笔记为脏数据。

export class DraftStore {
  private static drafts: Map<string, string> =
    new Map();

  static save(noteId: string, content: string): void {
    this.drafts.set(noteId, content);
  }

  static read(noteId: string): string | undefined {
    return this.drafts.get(noteId);
  }

  static remove(noteId: string): void {
    this.drafts.delete(noteId);
  }
}

页面里再把编辑动作接进来:

private onDraftChanged(value: string): void {
  this.draftContent = value;
  this.isDirty = true;

  DraftStore.save(
    this.selectedNoteId,
    value
  );

  AppStorage.setOrCreate(
    'draftState',
    'DIRTY'
  );
}

这层 Store 在当前 Demo 里只是进程内缓存。

它能解决窗口重排、组件重建导致的草稿丢失,但不能解决应用被系统终止以后恢复的问题。

正式笔记应用需要继续把草稿落到 Preferences、数据库或者文件层。这里我没有为了文章看起来“完整”,硬把所有持久化方案全塞进一个 Demo。

四、420vp 切到 840vp 时,我只做三件事

真正跑起来以后,窗口从 420vp 变成 840vp,我不再重新初始化页面,而是只做:

  1. 更新 windowWidthVp
  2. 更新 windowBreakpoint
  3. 让 Navigation 根据状态切换模式

HiLog 里最终固定成:

Window: 420vp -> 840vp
Mode: STACK -> SPLIT
Selected: note_20261001_07
Draft restored: 126 chars

这一张 DevEco 图里可以看到几个关键点。

左侧项目不是一个只有 Index.ets 的空 Demo,而是单独拆了:

common/
  BreakpointStore.ets
model/
  DraftStore.ets
pages/
  WorkspacePage.ets

中间代码监听 windowSizeChange,右侧展开屏已经进入双栏,底部日志则证明状态没有因为窗口变化而回到默认值。

如果日志里 Mode 对了,Selected 却变了,我会先查状态归属,不会继续调布局参数。

五、为什么我没有把所有逻辑交给 NavigationMode.Auto

Navigation 本身支持自适应模式,这个能力很实用。

但当前 Demo 仍然手动维护 COMPACT / EXPANDED,原因不是 Auto 不够好,而是我的业务还有额外状态需要跟随断点变化。

例如:

  • 小窗口隐藏详情辅助工具栏;
  • 大窗口固定显示最近笔记列表;
  • 600vp 以上展示项目目录;
  • 840vp 以上未来可能增加第三块辅助信息。

也就是说,Navigation 的 mode 只是其中一个消费者。

断点状态还会被其他组件使用。

如果一个项目只是简单的导航单栏 / 分栏,没有这些业务差异,直接使用 Auto 会更省事;如果页面同时有多块内容随窗口变化,保留一个业务断点层会更清楚。

六、窗口变化时,不要顺便重新拉一遍网络数据

这个问题我在旧项目里遇到过很多次。

有些页面把网络加载写在 aboutToAppear() 里,又因为布局切换造成页面局部重新创建,结果窗口变化一次,详情接口跟着又请求一次。

这对笔记编辑尤其危险。

服务器回来的旧正文,有可能覆盖本地还没提交的 126 个字符。

所以当前 Demo 里,草稿优先级明确高于远端正文:

private restoreNote(noteId: string): void {
  const localDraft = DraftStore.read(noteId);

  if (localDraft !== undefined) {
    this.draftContent = localDraft;
    this.isDirty = true;
    return;
  }

  this.loadRemoteNote(noteId);
}

这段代码解决的不是“离线同步”,而是一个更小的问题:

如果本地已经有未提交草稿,就不要因为窗口重排重新拿旧数据覆盖它。

正式项目里还需要版本号、更新时间、冲突处理,这些属于同步层逻辑。

七、最终运行结果看的是“连续”,不是“双栏成功”

最后手机运行图固定在 840vp 的展开结果。

当前状态是:

  • Window:840vp
  • Mode:SPLIT
  • Breakpoint:EXPANDED
  • Draft:DIRTY
  • Unsaved:126 chars

note_20261001_07 仍然是“项目会议纪要”,没有因为展开切回列表首页。

这才是我这次适配真正想验证的东西。

如果只看截图,双栏谁都能搭出来。

真正进入实际使用以后,用户会在折叠、展开、旋转、分屏之间来回切。每次变化都把输入内容清空一次,这种适配即使视觉上再漂亮,也很难算完成。

官方对折叠屏体验的核心要求里也强调状态连续。我的理解是:布局可以根据窗口变化,任务不能因为布局变化被打断。

八、这次留下的不是一个“折叠屏专用页面”

写完以后,我反而把 FoldNoteDesk 里的“折叠屏判断”删得越来越少。

最后真正保留下来的,是三层关系:

WindowStage 提供窗口变化 → Breakpoint 解释当前空间 → Navigation 和业务组件根据断点重排。

而笔记 ID、草稿、编辑状态不属于布局层。

它们应该独立存在。

这种结构的好处是,未来这个页面放到平板、自由窗口、桌面窗口里,仍然可以继续工作。

如果后续要扩展,我会继续补两块:

第一是把 DraftStore 换成真正的持久化草稿层。

第二是给断点切换加 UI 自动化验证,专门检查“窗口尺寸变化后 selectedNoteId 和 draft 长度不变”。

相比写一个“适配折叠屏”的 if/else,这两件事更像实际工程里应该继续做的工作。

参考资料

  • HarmonyOS 多设备通用适配指南
    https://developer.huawei.com/consumer/cn/multidevice/adaptive-apps/
  • Navigation 分栏开发
    https://developer.huawei.com/consumer/cn/doc/doccenter-capabilities/arkts-navigation-split-mode
  • 折叠屏设计原则
    https://developer.huawei.com/consumer/cn/doc/doccenter-ux-design/design-principles-0000001957023989
Logo

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

更多推荐