HarmonyOS 7.0 / API 26 DynamicLayout 实战:折叠屏和平板窗口变化后的布局稳定性排查 HarmonyOS 7.0 / API 26 DynamicLayout 实战折叠屏和平板窗口变化后的布局稳定性排查先讲问题不先堆概念做 HarmonyOS 多设备页面时最容易翻车的地方不是把 UI 画出来而是窗口尺寸变化以后页面还能不能稳住。手机竖屏、折叠屏展开、平板分屏、鸿蒙电脑窗口拖拽这几个场景看起来只是宽度变化实际会牵出一堆问题列表状态丢失、详情面板反复重建、弹窗位置错乱、输入框内容被清空、滚动位置回到顶部。这篇按 **HarmonyOS 7.0 / API 26** 的开发口径来写把 DynamicLayout 当成多设备页面的布局调度层来分析。重点不是“组件怎么写”而是页面从单栏切到双栏、从双栏回到单栏时状态、数据和组件生命周期怎么保持稳定。为什么普通断点写法容易出问题很多页面一开始会这么写拿到窗口宽度然后 if/else 切两套布局。代码很直观但页面复杂以后会出现三个问题。问题表现根因状态丢失搜索词、勾选、滚动位置没了单栏和双栏用了两套状态对象重复请求切换窗口后接口又跑一遍子组件被销毁再创建交互错位弹窗、悬浮按钮、侧栏位置不对布局变化和浮层定位没有统一调度所以我不建议把断点逻辑散落在每个组件里。更好的做法是把窗口宽度、设备形态和页面模式集中成一个 LayoutProfile然后页面只消费这个 profile。先定义布局画像布局画像要足够具体不要只有一个 isLargeScreen。因为折叠屏展开和平板横屏虽然都宽但交互重点不一样。前者要处理折痕和展开状态后者更关注分屏和窗口拖拽。type DeviceScene phone | foldable | tablet | desktopWindow; type LayoutMode singleColumn | masterDetail | threePane; interface WindowSnapshot { widthVp: number; heightVp: number; density: number; isFoldExpanded: boolean; isFreeWindow: boolean; } interface LayoutProfile { scene: DeviceScene; mode: LayoutMode; keepListAlive: boolean; keepDetailAlive: boolean; sidePanelWidthVp: number; reason: string; } export class DynamicLayoutProfileResolver { resolve(snapshot: WindowSnapshot): LayoutProfile { if (snapshot.isFreeWindow snapshot.widthVp 1200) { return { scene: desktopWindow, mode: threePane, keepListAlive: true, keepDetailAlive: true, sidePanelWidthVp: 360, reason: 鸿蒙电脑或大窗口适合三栏工作台结构 }; } if (snapshot.isFoldExpanded snapshot.widthVp 840) { return { scene: foldable, mode: masterDetail, keepListAlive: true, keepDetailAlive: true, sidePanelWidthVp: 320, reason: 折叠屏展开后列表和详情可以并排展示 }; } if (snapshot.widthVp 900) { return { scene: tablet, mode: masterDetail, keepListAlive: true, keepDetailAlive: true, sidePanelWidthVp: 340, reason: 平板横向空间足够适合主从结构 }; } return { scene: phone, mode: singleColumn, keepListAlive: true, keepDetailAlive: false, sidePanelWidthVp: 0, reason: 手机窄屏优先保证单列阅读和操作效率 }; } }这段代码的核心是布局变化要先变成可解释的数据再交给页面渲染。这样调试时能直接看到当前为什么是 singleColumn为什么切成 masterDetail而不是在一堆 if/else 里猜。案例一折叠屏展开后列表状态不能丢第一个复现场景用户在手机窄屏里搜了一个关键词滚动到列表中间这时展开折叠屏页面变成左列表右详情。如果列表组件被重新创建搜索词和滚动位置就会丢。我会把页面状态单独放在 Store 里不跟布局组件绑定。interface ListPageState { keyword: string; selectedId: string; scrollOffset: number; checkedIds: string[]; } export class LayoutStablePageStore { private state: ListPageState { keyword: , selectedId: , scrollOffset: 0, checkedIds: [] }; updateKeyword(keyword: string): void { this.state { ...this.state, keyword }; } selectItem(id: string): void { this.state { ...this.state, selectedId: id }; } saveScrollOffset(offset: number): void { this.state { ...this.state, scrollOffset: Math.max(0, offset) }; } toggleChecked(id: string): void { const exists this.state.checkedIds.includes(id); this.state { ...this.state, checkedIds: exists ? this.state.checkedIds.filter(item item ! id) : [...this.state.checkedIds, id] }; } snapshot(): ListPageState { return { ...this.state, checkedIds: [...this.state.checkedIds] }; } }布局从单栏变成双栏时页面只换展示结构不换状态来源。这样搜索词、选中项、勾选项和滚动位置都能保留下来。验证方式const resolver new DynamicLayoutProfileResolver(); const store new LayoutStablePageStore(); store.updateKeyword(性能优化); store.selectItem(article-1001); store.saveScrollOffset(620); store.toggleChecked(article-1001); const phoneProfile resolver.resolve({ widthVp: 390, heightVp: 780, density: 3, isFoldExpanded: false, isFreeWindow: false }); const foldProfile resolver.resolve({ widthVp: 980, heightVp: 760, density: 2.5, isFoldExpanded: true, isFreeWindow: false }); console.info(phoneProfile.mode); // singleColumn console.info(foldProfile.mode); // masterDetail console.info(store.snapshot()); // keyword、selectedId、scrollOffset、checkedIds 都还在这组验证看的是状态稳定性不是组件长得好不好看。只要切布局后状态没丢第一层就过了。案例二平板分屏宽度变化详情面板不能反复请求第二个场景是平板分屏。用户把应用从大窗口拖成窄窗口再拖回来。如果详情组件每次都重新请求数据页面会卡接口也浪费。我会给详情数据加一层缓存并且把“是否需要重新加载”交给布局控制器判断。interface DetailCacheItem { id: string; loadedAt: number; payload: Recordstring, string | number | boolean; } export class DetailPanelCache { private cache new Mapstring, DetailCacheItem(); private maxAgeMs 5 * 60 * 1000; get(id: string): DetailCacheItem | undefined { const item this.cache.get(id); if (!item) return undefined; if (Date.now() - item.loadedAt this.maxAgeMs) { this.cache.delete(id); return undefined; } return item; } set(id: string, payload: Recordstring, string | number | boolean): void { this.cache.set(id, { id, payload, loadedAt: Date.now() }); } shouldReload(id: string, profile: LayoutProfile): boolean { if (!id) return false; if (profile.mode singleColumn !profile.keepDetailAlive) return false; return !this.get(id); } }这段代码解决的是“窗口变化不等于数据过期”。从 masterDetail 切到 singleColumn 时详情面板可以暂时不展示但数据不一定要清掉。等用户再回到大窗口时能直接恢复不需要重新请求。页面组合方式页面层只做三件事读取窗口状态、生成布局画像、根据画像决定单栏还是主从结构。业务状态和数据缓存都在外面。Component struct AdaptiveWorkbenchPage { private resolver: DynamicLayoutProfileResolver new DynamicLayoutProfileResolver(); private store: LayoutStablePageStore new LayoutStablePageStore(); private detailCache: DetailPanelCache new DetailPanelCache(); State private profile: LayoutProfile this.resolver.resolve({ widthVp: 390, heightVp: 780, density: 3, isFoldExpanded: false, isFreeWindow: false }); build() { Column() { if (this.profile.mode singleColumn) { this.buildSingleColumn(); } else if (this.profile.mode masterDetail) { this.buildMasterDetail(); } else { this.buildThreePane(); } } .width(100%) .height(100%) } Builder buildSingleColumn() { Column() { Text(列表) Text(this.profile.reason) } } Builder buildMasterDetail() { Row() { Column() { Text(列表) }.width(38%) Column() { Text(详情) }.layoutWeight(1) } } Builder buildThreePane() { Row() { Column() { Text(导航) }.width(240) Column() { Text(列表) }.width(360) Column() { Text(详情) }.layoutWeight(1) } } }真实项目里 DynamicLayout 可以承接更细的布局调度但这段示例想强调的是不要让状态跟着布局一起销毁。布局负责摆放Store 负责状态Cache 负责数据复用。方案对比方案优点风险适用场景每个页面自己写 if/else快状态容易丢重复逻辑多Demo 或简单页面全局只用 isLargeScreen简单折叠屏和平板差异被抹平轻量适配LayoutProfile 集中调度可解释、可测试、可复用需要先设计结构正式多设备应用所有页面强行三栏看起来高级手机和小窗体验差不建议我会选 LayoutProfile。它不是为了多写一层类而是为了让页面在设备变化时有稳定依据。后面加折痕区域、窗口拖拽、平板分屏、鸿蒙电脑窗口态都可以继续往 profile 上扩。验收清单验收项通过标准手机窄屏singleColumn详情不常驻折叠屏展开masterDetail列表状态不丢平板横屏masterDetail详情数据不重复请求大窗口threePane导航、列表、详情分区明确窗口来回拖拽搜索词、选中项、滚动位置保留详情缓存未过期时不重复请求如果这几项没有过说明布局只是“看起来适配”还没有真正适配多设备。结论HarmonyOS 7.0 / API 26 做多设备页面重点不是多写几套布局而是把布局变化、状态保持和数据复用分开。DynamicLayout 或类似动态布局能力负责把页面摆稳LayoutProfile 负责解释当前为什么这样摆Store 和 Cache 负责让用户操作不中断。开发者真正要防的是窗口一变页面全重来。只要状态和数据不跟着布局销毁多设备适配就会稳很多。