HarmonyOS 7.0 / API 26 DynamicLayout 实战:折叠屏和平板窗口变化后的布局稳定性排查

HarmonyOS 7.0 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: Record<string, string | number | boolean>;
}

export class DetailPanelCache {
  private cache = new Map<string, 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: Record<string, 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 负责让用户操作不中断。

开发者真正要防的是:窗口一变,页面全重来。只要状态和数据不跟着布局销毁,多设备适配就会稳很多。

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐