折叠屏早就不是发布会上的概念机了,身边拿三星 Z Fold、华为 Mate X 系列、小米 MIX Fold 当主力机的人越来越多,OPPO Find N 也有一批忠实用户。但很多 Flutter 开发者还没意识到,折叠屏不能简单粗暴地当成“大屏手机”来处理。屏幕从合上到展开,宽度可能从 3 英寸跳到 7 英寸,窗口尺寸变了、可见区域变了、用户的使用姿势也变了,如果不做适配,你的 App 展开后要么内容被拉伸得没法看,要么右侧一大块空白浪费掉,甚至切换姿态后页面状态直接丢没了,体验相当糟糕。
这篇文章我想从实操角度聊透折叠屏适配这件事,核心就是标题里写的三件事:窗口布局、双栏路由、状态连续性。我会用自己实际跑过的项目代码来拆解,不会只停留在“要适配”的层面,而是告诉你每一步为什么这么做、代码怎么写、遇到问题怎么排查。不管你是刚开始做适配,还是已经被折叠屏设备上的疑难杂症折磨过,这篇文章应该都能给你一些直接能用的东西。
1. 折叠屏适配整体思路:先想清楚三个问题
1.1 折叠屏到底改变了什么
很多人觉得折叠屏适配就是把 UI 做得“大一点”,其实没那么简单。折叠屏带来的变化不只是屏幕尺寸变大,而是整个交互范式的改变。展开状态下,屏幕宽度通常在 600dp 到 800dp 左右,这个宽度刚好够放两栏内容,也就是 Master-Detail 布局。而合上时就是一台普通手机,宽度 360dp 左右,只能显示单栏。
问题的关键点在于:设备尺寸变化不是静态的,用户可能在展开状态下用着,然后直接合上屏幕,或者从合上状态展开继续操作。这意味着你的 App 在运行时可能随时要切换布局模式。如果按传统方式写死一套布局,切姿态时就会出现 UI 变形、状态丢失、导航混乱等一连串问题。
我在第一次做适配时犯过这样的错误:用MediaQuery.of(context).size.width判断了屏宽,展开了就显示双栏,但没监听后续的尺寸变化。结果用户展开后旋转了一下设备,布局直接乱掉。这个教训说明,折叠屏适配不是“适配一次”,而是“适配每一刻”。
1.2 断点布局与 Material 3 自适应范式
要做折叠屏适配,第一步是建立断点思维。参考 Material Design 3 的自适应布局规范,屏幕宽度大致分成三个区间:宽度小于 600dp 的 Compact 区间,对应手机合上状态;600dp 到 839dp 的 Medium 区间,对应折叠屏展开或平板竖屏;840dp 及以上的 Expanded 区间,对应平板横屏或大尺寸折叠屏展开。
断点的作用不是硬性规定,而是给你一个布局切换的参考阈值。我自己的项目经验是:折叠屏展开后宽度一般在 673dp 到 720dp 左右,正好落在 Medium 区间,这个场景用双栏布局最合适。单个页面内容如果超过 840dp,可以考虑三栏布局,比如邮件类 App 的“列表-详情-预览”三栏结构。
断点选好之后,布局策略要遵循一个原则:同一种逻辑数据,根据宽度用不同的可视层级来呈现。窄屏时低层级的内容占满整个屏幕,宽屏时多层内容并行展示。Flutter 里面实现这个思路,主流方式是LayoutBuilder加MediaQuery,再配合Scaffold的各个组件灵活组合。
1.3 技术选型:纯 Flutter 方案还是混合方案
折叠屏适配在技术路径上基本有两条路。一条是纯 Flutter 方案,用MediaQuery、LayoutBuilder、DisplayFeature这些框架自带的 API 来完成布局检测和姿态判断。另一条是通过平台通道调用 Android 原生 WindowManager 的 Jetpack WindowManager 库,获取铰链角度、折叠姿态等更细粒度的硬件信息。
大部分应用场景下,纯 Flutter 方案就够了。Flutter 从 3.x 开始对折叠屏的支持越来越完善,MediaQuery.displayFeatures可以直接拿到折痕区域信息,配合SafeArea能做到很好的适配效果。只有像视频播放器配合折叠姿态动态调整界面,或者需要利用折叠角度模拟视差这类高级需求,才值得走原生通道去拿精确的角度数据。
我建议你优先把纯 Flutter 方案跑通,再根据实际需求决定要不要上原生扩展。一来减少平台交互的代码量,二来避免在 Android 和 iOS 双端各维护一套原生逻辑,维护成本低很多。
2. 窗口布局:从单一画布到响应式画布
2.1 获取窗口尺寸的正确姿势
Flutter 里获取窗口尺寸,最基础的写法是MediaQuery.of(context).size。但这里有个性能细节,从 Flutter 3.10 开始,官方推荐使用MediaQuery.sizeOf(context)。区别在哪呢?MediaQuery.of(context)会监听整个 MediaQuery 对象的变化,任何一项属性改变(比如文字缩放比例变了)都会触发依赖它的组件重新构建。而MediaQuery.sizeOf(context)是精细化依赖,只在 size 变化时才会重建,性能更有优势。
写响应式布局时,我一般用LayoutBuilder包住根组件,它能拿到施加约束后的具体尺寸,比 MediaQuery 更灵活。MediaQuery 倾向于描述全局窗口特征,LayoutBuilder 则关注实际布局区域,在嵌套导航或抽屉面板里,两者给出的宽度可能是不同的。
import 'package:flutter/material.dart'; class AdaptiveScaffold extends StatelessWidget { const AdaptiveScaffold({super.key}); @override Widget build(BuildContext context) { return LayoutBuilder( builder: (context, constraints) { final width = constraints.maxWidth; final isExpanded = width >= 600; // 调试时可打印宽度,确认断点命中是否精确 // debugPrint('当前布局宽度: $width,双栏: $isExpanded'); return Scaffold( body: isExpanded ? _buildWideLayout() : _buildNarrowLayout(), ); }, ); } }这里要提醒一点:不要直接拿constraints.maxWidth和魔法数字600一把梭,建议定义成静态常量,比如static const double expandedBreakpoint = 840;和static const double mediumBreakpoint = 600;,后续调整断点的时候只改一个地方。我踩过这种坑:三个文件里各写了一个600,产品说“平板试试 700 效果更好”,结果全局搜出来改了半天。
2.2 断点设计与布局切换策略
断点确定之后,要明确每个区间具体怎么呈现内容。拿一个典型的资讯类 App 举例:窄屏时底部导航栏对应四个 Tab,首页 Tab 点进去是列表,点击列表项 push 一个详情页;宽屏时左侧显示导航菜单,中间是列表,右侧直接预览详情内容。
具体到 Flutter 代码层面,窄屏和宽屏的差异主要体现在三个方面。第一个是导航结构的差异:窄屏用BottomNavigationBar或NavigationBar,宽屏用NavigationRail。第二个是页面组织差异:窄屏用 Navigator 压栈跳详情,宽屏用 Row 并排展示。第三个是内容密度差异:宽屏可以展示两列卡片甚至三列,窄屏保持单列滚动。
Widget _buildScaffoldBody(BuildContext context, bool isExpanded) { return isExpanded ? Row( children: [ const NavigationRail( destinations: [ NavigationRailDestination( icon: Icon(Icons.home_outlined), selectedIcon: Icon(Icons.home), label: Text('首页'), ), NavigationRailDestination( icon: Icon(Icons.person_outline), selectedIcon: Icon(Icons.person), label: Text('我的'), ), ], ), const VerticalDivider(width: 1), Expanded(child: _buildMasterPanel()), ], ) : Scaffold( body: _buildMasterPanel(), bottomNavigationBar: NavigationBar( destinations: const [ NavigationDestination(icon: Icon(Icons.home_outlined), label: '首页'), NavigationDestination(icon: Icon(Icons.person_outline), label: '我的'), ], ), ); }布局切换策略上,有个非常容易踩的坑:不要每次构建都凭空创建新的 widget 树。如果宽屏和窄屏下组件差异太大,建议拆分成独立的 widget 类,避免在 build 方法里写大量三目嵌套,否则代码可读性会迅速恶化。另外,切换布局时最好加一点动画过渡,比如用AnimatedSwitcher包裹主体区域,不然展开/合上的瞬间会显得非常生硬。
2.3 铰链区与安全区的处理
折叠屏展开后,屏幕中间可能有一条竖折痕或者铰链区域。如果你的 UI 布局恰好跨越这条铰链,文字、图标、按钮可能会被折痕影响观感,点击区域也可能落在物理铰链的缝隙上,导致触摸不灵敏。Flutter 侧处理这类问题,核心 API 是MediaQuery.displayFeatures,它里面可能包含类型为DisplayFeatureType.hinge的区域。
final displayFeatures = MediaQuery.displayFeaturesOf(context); final hinge = displayFeatures.where( (f) => f.type == DisplayFeatureType.hinge, ).toList();当检测到铰链区域且设备处于展开状态时,布局策略要考虑几个方向。第一,如果窗格分割线和铰链位置重叠,就可以在铰链位置放一条竖分割线;第二,如果铰链把屏幕分成左右两部分,可以分别对左右区域做独立的 SafeArea 处理,避免内容挤进铰链下方的微型凹陷区;第三,如果姿态是屏幕半折成帐篷式,铰链变成水平的,那么上下区域都要做避让。
安全区方面,不要只依赖系统的 SafeArea,折叠屏设备在展开和合上两种形态下,系统导航栏的呈现方式可能不同,状态栏高度也可能因为屏占比变化产生波动。建议在根布局上使用SafeArea(top: true, bottom: true, left: false, right: false)控制必要的方向,而不是四个方向全加上,否则宽屏模式左右会各留一条多余的白边,看起来极其不协调。
3. 双栏路由:Master-Detail 的正确打开方式
3.1 为什么简单的 Row + ListView 不够用
折叠屏展开后,最常见的需求就是左侧列表、右侧详情这种双栏 Master-Detail 结构。很多人的第一反应是用Row放一个ListView和详情 widget,看起来好像实现了。但仔细一想你会发现:右侧详情页如果还需要继续跳转下一层呢?详情页里的某个按钮点击后要打开一个新页面,这个页面应该显示在右侧区域,而不是覆盖整个屏幕。你用 Row 铺出来的静态结构,根本没有“导航栈”的概念,页面跳转的逻辑完全无处安放。
另一个问题是返回键的处理。单栏手机模式,用户按返回键应该从详情返回上一级列表;双栏模式,左侧列表和右侧详情是并行可见的,用户按返回键时到底该关哪个页面?如果不用独立导航栈,返回键行为会完全失控。我在测试时遇到过,展开状态下按返回键,直接把整个 App 退到后台了,这显然不是用户期望的行为。
3.2 嵌套 Navigator 实现双栏路由
要解决导航问题,核心手段是使用嵌套的 Navigator。主 Navigator 管理最外层页面,比如从列表页跳转到独立全屏页面;右侧详情区域内部再包一个 Navigator 作为二级导航栈,专门管理详情区内的页面跳转。
具体方案:左侧列表项点击时,不再调用顶层Navigator.push,而是通过右侧 Navigator 的 key 做 push。这样详情页的跳转被限制在右侧区域内部,返回键击中时也只会 pop 详情区域的导航栈,不会影响到左侧列表。
final _detailNavigatorKey = GlobalKey<NavigatorState>(); Widget _buildWideLayout() { return Row( children: [ SizedBox( width: 320, child: MasterList( onItemTap: (item) => _openDetail(item), ), ), VerticalDivider(width: 1), Expanded( child: Navigator( key: _detailNavigatorKey, onGenerateRoute: (settings) { return MaterialPageRoute( settings: settings, builder: (_) => const EmptyDetailPlaceholder(), ); }, ), ), ], ); } void _openDetail(Item item) { _detailNavigatorKey.currentState?.push( MaterialPageRoute( builder: (_) => DetailPage(item: item), ), ); }窄屏的时候,点击列表项要走主 Navigator 的全屏跳转;宽屏的时候走详情区域 Navigator 的局部跳转。两种模式代码路径不同,所以在列表项的点击回调里要区分当前是宽屏还是窄屏模式。实现方式可以是传递一个bool isWideScreen到列表组件,也可以是列表组件内部自己用MediaQuery.sizeOf(context)判断,两者都能用,我倾向于后者,减少参数透传。
双栏路由有一个细节值得强调:右侧详情区域即使是空的,也要有一个默认路由。比如首次进入宽屏布局,还没点击任何列表项,右侧导航栈里可以放一个“选择左侧项目查看详情”的占位页。这一步不仅是美观问题,还关系到返回键行为是否正常,如果右侧导航栈为空,按返回键时 Navigator 会不知道往哪走,可能出现异常。
3.3 折叠与恢复:导航栈迁移
姿态切换时,导航结构要从单栏变成双栏,或从双栏变成单栏,这时候导航栈的迁移就成了最棘手的问题。
举个例子:用户用单栏模式浏览,从首页进入列表 A,再从列表 A 进入详情 B,此时导航栈是 [首页, 列表A, 详情B]。用户直接展开屏幕,变成宽屏双栏模式。理想状态下,左侧应该是列表 A,右侧应该是详情 B。但如果你在布局切换时重新创建了页面,导航栈直接就丢了,用户看到一片空白,非常崩溃。
解决思路可以从状态管理层面入手。布局切换时不销毁页面数据,把当前页面栈的状态提升到上层组件,重建布局时按需重新装载。具体做法:用页面栈的 List 对象记录当前打开了哪些页面,每个页面项包含要打开的路由名称和参数。宽屏渲染时,列表左侧显示 Master,右侧 Navigator 根据页面栈的末尾元素重建详情页。
class DetailStackController extends ChangeNotifier { final List<DetailRouteInfo> _stack = []; List<DetailRouteInfo> get stack => List.unmodifiable(_stack); void push(DetailRouteInfo info) { _stack.add(info); notifyListeners(); } void pop() { if (_stack.length > 1) { _stack.removeLast(); notifyListeners(); } } void restore(List<DetailRouteInfo> restored) { _stack ..clear() ..addAll(restored); notifyListeners(); } }窄屏切宽屏时,把主 Navigator 的当前路由栈解析成这个堆栈列表;宽屏切窄屏时,把详情区域的堆栈同步回主 Navigator。整个过程需要妥善处理的地方不少,建议配合状态管理库使用,比如 Provider、Riverpod 或 Bloc 都可以,核心是统一管理这份页面栈数据。我自己实测下来,用 Riverpod 的组合方式写起来最顺手,页面栈的读写逻辑清晰,调试时也能在 DevTools 里一眼看到状态变化。
4. 状态连续性:折叠切换不丢状态
4.1 配置变更如何“杀掉”你的页面状态
Android 系统在屏幕尺寸、方向、字体缩放、深色模式等配置变化时,默认行为是销毁并重建当前 Activity。折叠屏的展开和合上,本质上就是一次 Configuration Change,伴随屏幕宽度从窄到宽或从宽到窄的变化。对于 Android 原生开发,处理方案是修改AndroidManifest.xml,给 Activity 配置configChanges属性,让系统在配置变化时不重建 Activity。
但 Flutter 场景下有个特殊情况:Flutter 的 Activity 即使不重建,Flutter 引擎内部视图仍然会跟随窗口尺寸变化,所以布局层面的适配没问题,但页面状态不一定能自动保住。比如滚动位置、表单输入内容、Tab 选中项、列表点击进来的数据,这些状态如果不做处理,布局切换时完全可能被重置。
我做了一遍测试得到的结论是:在 Flutter 3.16 以上的版本中,同一 Activity 内折叠触发的尺寸变化,普通 State 里的非可见状态(比如 int 计数、bool 开关)大多能保留,但依赖 BuildContext 的状态和路由栈里的页面状态则不一定安全。最稳妥的策略是显式保存并恢复关键状态,而不是侥幸依赖框架行为。
4.2 PageStorage 与 ScrollPosition 恢复
Flutter 的PageStorage是处理滚动位置恢复的利器。ListView、GridView等滚动组件默认会通过PageStorageKey保存滚动偏移,页面重建时自动恢复。这个机制在 Tab 切换和页面进出场景非常有用,折叠屏适配时也能派上用场。
为了让滚动位置在布局切换时可靠恢复,需要给滚动列表设置稳定的 PageStorageKey:
ListView.builder( key: const PageStorageKey<String>('home_feed_list'), itemCount: items.length, itemBuilder: (context, index) => ListTile( title: Text(items[index].title), ), )这里有一个隐藏的坑:PageStorageKey 保存的数据是全局的,不同页面如果用了相同的 key,滚动位置会互相污染。我见过同一 GroupKey 下两个列表互相串位置的案例,排查了半天才发现是两个页面的 ListView 都用了同一个字符串 key。建议 key 命名时带上页面标识,比如home_feed_list、category_list_${categoryId},避免跨页面冲突。
除了滚动位置,ScrollController的初始化偏移也可以在做恢复时手动指定。如果你用的是自定义滚动行为,不能依赖 PageStorage 自动恢复,那可以在 didChangeDependencies 或路由重建时,根据保存的偏移量执行 jumpTo。
4.3 状态提升与 RestorationMixin 组合方案
滚动位置只是状态连续性的一小块拼图。折叠屏切换时,页面的业务状态、路由栈、表单填写内容、筛选条件等都需要保持连续。从架构层面看,最有效的策略是把那些“不能丢”的状态从页面组件中提出来,放到布局切换后依然存活的上层对象中。
具体来说,我把页面状态分成了三类。第一类是可丢弃状态,比如展开/收起的面板开关,丢了也无所谓;第二类是用户敏感状态,比如正在编辑的草稿,必须恢复;第三类是导航状态,比如当前打开了哪个详情页,必须恢复。第一类用局部 State 就行,第二类和第三类必须做状态提升或者持久化。
对于编辑类页面,建议用RestorationMixin配合RestorationBucket实现状态自动恢复。这个机制利用 Flutter 的 RestorationManager,在 Activity 因为配置变更重建或进程被回收后,自动恢复标记过字段的值。使用方法不复杂:
class EditorPage extends StatefulWidget { const EditorPage({super.key}); @override State<EditorPage> createState() => _EditorPageState(); } class _EditorPageState extends State<EditorPage> with RestorationMixin { final RestorableString _draftContent = RestorableString(''); @override String? get restorationId => 'editor_page'; @override void restoreState(RestorationBucket? oldBucket, bool initialRestore) { registerForRestoration(_draftContent, 'draft_content'); } @override void dispose() { _draftContent.dispose(); super.dispose(); } }注意 restorationId 在整个应用内必须唯一。如果确保唯一,建议在页面类内加一个 static 的字符串常量来管理,避免散落各处。
4.4 多分支状态同步:左列表和右详情的联动
双栏模式下还有一个状态同步问题:左侧列表选中了某项,右侧详情展示对应内容,这两边其实是对同一份“选中状态”的两个视图。如果处理不好,展开时左边选中的是 A,右边显示的却是 B,或者折叠再展开后,选中项被重置了。
我的方案是引入一个页面级的控制器,把“当前选中项 ID”作为核心状态统一管理。左侧列表点击时更新控制器,右侧详情页监听控制器的变化刷新内容。这样不管宽度怎么切换,竞态条件消除了,状态连续性也就有了保障。
final selectedItemProvider = StateProvider<Item?>((ref) => null);这个方案的另一个好处是:布局切换时,细节页可以从这个顶部状态重新生成内容,而不依赖路由参数。即使右侧详情区域的 Navigator 被重建,只要选中项 ID 还在,详情内容也能正确恢复。这也是我在实战中发现的最实用的状态连续性手段之一。
5. 折叠屏姿态检测与其他坑
5.1 铰链、帐篷、书脊:姿态检测 API
除了宽度布局,折叠屏还有一些特有的硬件姿态。常见的有桌面模式(平放)、帐篷模式(屏幕朝外,像帐篷一样架在桌面上)、书脊模式(屏幕朝内半折,书脊朝上)。不同姿态适合不同的功能交互,比如视频播放器在帐篷模式下应该弹出全屏播放器,桌面模式下可以显示中控台形态的 UI。
要在 Flutter 里检测这些姿态,核心还是MediaQuery.displayFeatures。铰链区域如果横跨屏幕,且设备处于半折叠状态,FoldingFeature里能拿到铰链的 bounds、state 和 orientation。再结合传感器数据,可以推断出大致姿态。不过这里的检测逻辑需要结合设备状态做综合判断,并不能直接拿到一个“帐篷模式”的枚举。
for (final feature in MediaQuery.displayFeaturesOf(context)) { if (feature is DisplayFeature && feature.type == DisplayFeatureType.hinge) { final hingeRect = feature.bounds; final isVertical = feature.orientation == DisplayFeatureOrientation.vertical; // 根据铰链方向、屏幕宽高比等推断当前姿态 } }5.2 分屏与多窗口的兼容性
折叠屏展开后,很多用户会主动启用分屏功能,把屏幕分成上下或左右两块区域,同时运行两个 App。这个时候,你的 App 窗口宽度会骤减,比如宽屏双栏布局切到分屏模式后可能只有一半甚至三分之一宽度。适配方案和折叠屏核心逻辑基本一致:用断点响应式布局处理宽度骤变。需要注意的是,分屏时不要强制 App 退出折叠模式,而是动态按照当前窗口宽度重新布局。
Flutter 对多窗口支持的整体状况是:尺寸变化可以监听,MediaQuery会自动反应窗口大小变化,断点逻辑会重新触发。但是 PlatformChannel 层面的东西要小心,某些插件可能缓存了旧窗口尺寸。我遇到过地图插件在分屏拖拽过程中出现尺寸错位,处理方案是监听窗口尺寸变化后主动通知插件更新,或者干脆在变化时重构插件视图。
6. 常见问题与排查技巧
6.1 折叠屏模拟器与真机测试建议
做折叠屏适配,真机当然是首选,但不可能每个开发者手里都有最新折叠屏手机。好在我们有折叠屏模拟器可以用。Android Studio 从 Electric Eel 版本开始内置了可折叠设备的模拟器镜像,提供了 Z Fold 类似的屏幕参数,支持切换折叠状态、调整铰链角度等。Flutter 侧启动模拟器后,直接跑flutter run就可以测试。
测试时建议把这些场景过一遍:折叠态启动 App、展开态启动 App、展开后合上、合上后展开、展开态旋转屏幕、分屏拖拽改变窗口大小、切换深色模式。每次切换都检查布局是否正常、状态是否丢失、导航是否错乱。这里分享一个小技巧:在调试模式打一个叠加层,实时显示当前窗口宽度和活动断点,能极大提升调试效率。
if (kDebugMode) { return SafeArea( child: Align( alignment: Alignment.topRight, child: Container( padding: const EdgeInsets.all(8), color: Colors.black54, child: Text('W: ${constraints.maxWidth.round()} dp'), ), ), ); }6.2 常见问题速查
| 问题现象 | 可能原因 | 解决方案 |
|---|---|---|
| 展开后内容横向拉伸变形 | 没做断点判断,直接拉伸填充 | 按宽度切换 Compact/Medium/Expanded 布局 |
| 折叠再展开后导航栈丢失 | 主路由栈和详情栈切换时未同步 | 状态提升 + 页面栈控制器统一管理 |
| 滚动位置重置 | 滚动列表缺少 PageStorageKey | 给 ListView/GridView 添加稳定的 PageStorageKey |
| 点击区域在折痕处失灵 | 内容布局跨越铰链区域 | 检测 DisplayFeature 后用 SafeArea 或分割线避让 |
| 宽屏模式左右白边 | SafeArea 四方向全开 | 按需禁用左右 SafeArea |
6.3 状态管理库选型与版本兼容性
最后聊一个选型层面的问题。折叠屏适配涉及较多跨组件状态同步和导航栈管理,建议至少在项目里有统一的全局状态容器。我测试过 Provider、Riverpod、GetX 和 Bloc,在折叠屏场景下都跑得通,关键不在于选哪个库,而在于把窗口宽度、选中项、导航栈这三类核心状态放在同一层级管理,避免散落在各个页面。
版本兼容方面需要注意:MediaQuery.sizeOf、MediaQuery.displayFeaturesOf这类新 API 要求 Flutter 3.10 以上。如果你的项目还在 Flutter 2.x 或早期 3.x 版本,建议升级后再做适配,否则会遇到 API 不存在或行为不一致的问题。另外DisplayFeatureType枚举的具体字段名在不同 Flutter 版本里也有过微小调整,升级大版本后记得全局搜索一下用法。
我在实际适配过程中踩过不少版本的坑,比如 Flutter 3.7 上MediaQuery.displayFeatures在某些国产定制 ROM 上拿不到数据,升级到 3.16 之后就稳定了。如果你发现姿态检测数据异常,优先检查 Flutter 版本和 Android 系统版本是否符合官方支持范围,不要一上来就怀疑自己的代码逻辑。
折叠屏适配总体来说不是一项“绝顶技巧”,它更考验你对布局、路由、状态这三件事的理解深度。把断点布局打牢、把嵌套导航做稳、把状态提升做彻底,你的 App 在折叠屏上的表现自然就不会差。最后分享一个个人习惯:每写完一个响应式页面,我都会在模拟器里来回折叠屏幕折腾十分钟,这段“折腾”往往能发现不少意想不到的 bug。