HarmonyOS 7 / API 26 折叠屏适配实战:窗口断点、双栏切换和状态保留一次验清 折叠屏适配最容易出问题的地方不是页面能不能铺满屏幕而是窗口变化以后状态还能不能保住。用户从外屏切到内屏或者在平板、鸿蒙电脑窗口里拖动宽度页面结构可能从单栏变成双栏。如果列表选中项、滚动位置、详情页参数都丢了体验会很差。这篇按 HarmonyOS 7 / API 26 的多设备适配思路来拆先把窗口宽度转成断点再把单双栏结构和业务状态分开最后用两条可复现路径验证。先定义断点不要到处写魔法数字我一般会先把窗口宽度分成三档compact、medium、expanded。断点典型场景页面结构compact手机竖屏、折叠屏外屏单栏列表详情新页打开medium折叠屏半展开、小平板列表加轻量详情预览expanded平板横屏、鸿蒙电脑窗口左列表右详情双栏断点判断单独封装后面页面只消费结果。~~~tstype WindowBreakpoint compact | medium | expandedfunction getBreakpoint(widthVp: number): WindowBreakpoint {if (widthVp 600) {return compact}if (widthVp 840) {return medium}return expanded}~~~这样做的好处是清楚。后面产品说 840 这个点要调整也只改一个地方。页面状态不要绑死在布局里问题写法通常是单栏页面有一套状态双栏页面又有一套状态。窗口一变组件重建状态跟着丢。~~~tstype RecipeListState {selectedId?: stringscrollIndex: numberkeyword: string}class RecipeStateStore {private state: RecipeListState {selectedId: undefined,scrollIndex: 0,keyword: }snapshot(): RecipeListState {return { ...this.state }}update(next: PartialRecipeListState): void {this.state { ...this.state, ...next }}}~~~这个 store 不关心当前是单栏还是双栏它只保存“用户在看什么”。布局切换时页面从这里恢复。案例一外屏单栏切到内屏双栏单栏时用户点列表进入详情展开后应该变成左列表右详情而且仍然选中刚才那条。~~~tsclass AdaptiveRecipePage {private store new RecipeStateStore()private breakpoint: WindowBreakpoint compactonWindowSizeChange(widthVp: number): void {const next getBreakpoint(widthVp)if (next this.breakpoint) {return}const snapshot this.store.snapshot()this.breakpoint nextthis.renderByBreakpoint(snapshot)}selectRecipe(id: string): void {this.store.update({ selectedId: id })const snapshot this.store.snapshot()this.renderByBreakpoint(snapshot)}private renderByBreakpoint(state: RecipeListState): void {if (this.breakpoint compact) {renderSingleColumn(state.selectedId)return}renderTwoColumn(state.selectedId)}}~~~这里的关键是窗口变化只改变布局不重置 selectedId。状态和布局分开以后单双栏切换就不会把用户当前选择冲掉。案例二拖动窗口宽度后滚动位置丢失平板或鸿蒙电脑窗口拖动宽度时列表可能重排。如果滚动位置没有单独保存页面会跳回顶部。~~~tsclass ListScrollKeeper {private lastIndex 0onScroll(index: number): void {this.lastIndex index}restore(): number {return this.lastIndex}}class AdaptiveListController {private scrollKeeper new ListScrollKeeper()onVisibleAreaChange(firstVisibleIndex: number): void {this.scrollKeeper.onScroll(firstVisibleIndex)}onLayoutRebuild(): void {const index this.scrollKeeper.restore()scrollToIndex(index)}}~~~滚动位置不一定要保存像素值。很多列表在数据稳定时保存 firstVisibleIndex 更实用适配重排后也更容易恢复。断点切换要有日志适配问题如果没有日志很难知道到底是窗口事件没触发还是触发后状态没恢复。~~~tstype BreakpointTrace {oldValue: WindowBreakpointnewValue: WindowBreakpointwidthVp: numberselectedId?: stringscrollIndex: number}function printBreakpointTrace(trace: BreakpointTrace): void {console.info([breakpoint],trace.oldValue - trace.newValue,width trace.widthVp,selected (trace.selectedId ?? none),scroll trace.scrollIndex)}~~~每次窗口变化都输出一次回归时能直接看出状态是否被保住。本地验证脚本先不接真实 UI也可以验证状态逻辑。~~~tsfunction verifyFoldableState(): void {const store new RecipeStateStore()store.update({ selectedId: recipe-12, scrollIndex: 40, keyword: noodle })const compact getBreakpoint(390)const expanded getBreakpoint(900)const snapshot store.snapshot()console.info([verify-breakpoint], compact, expanded)console.info([verify-state], snapshot.selectedId, snapshot.scrollIndex, snapshot.keyword)}~~~预期结果是断点从 compact 到 expanded但 selectedId、scrollIndex、keyword 都不丢。方案对比方案好处问题布局里直接放状态写起来快窗口一变容易丢状态全局 store 保存所有状态恢复方便容易过度设计页面级状态仓库状态清楚适配成本低需要维护快照和恢复入口我更推荐页面级状态仓库。它比把状态散在组件里稳也比把所有东西塞进全局 store 更轻。回归检查场景操作通过标准外屏到内屏选中一条后展开双栏仍显示同一条详情拖动窗口列表滚动到中间再改宽度滚动位置不回到顶部搜索后切换输入关键词再切断点关键词和筛选结果保留详情返回双栏切回单栏后返回路由不乱跳不重复打开详情小结HarmonyOS 7 / API 26 做折叠屏和多窗口适配重点不是把页面拉宽而是窗口变化以后状态不丢。把断点判断、页面状态、滚动恢复和日志分开单双栏切换就会稳定很多。后面再遇到折叠屏适配问题也能直接从断点、状态、滚动三个位置排查。