Flutter 折叠屏 iPad / 宽屏适配实践
最近业内关于 iPhone 推出折叠设备的传闻愈演愈烈。作为开发者,与其等风来,不如先修路。 如果你正在做 折叠屏 iPad / 宽屏适配,而且项目已经跑了一段时间,这篇文章大概率就是写给你的。
你可能正处在这样的背景里:
- 项目长期以一套手机设计稿为主,当前基准是
375 x 667 - 页面数量不少,不现实为折叠展开态和平板单独维护一套 UI
- 工程里已经大量使用
flutter_screenutil,不想为了宽屏适配推翻整套尺寸体系 - 现在遇到的问题很具体:展开后控件变大、留白变多、Grid 比例失真、横向卡片太宽、某些标题或容器开始裁切
如果你和上面的情况接近,那么这篇文章想解决的就不是"如何从零认识 flutter_screenutil",而是另一个更实际的问题:
在保留原有手机设计稿和尺寸体系的前提下,怎么把折叠屏 / 宽屏适配做得可控、可维护,而且不用把页面重写一遍。
下面总结的是一套已经在真实业务工程中落地的方案。它不是单讲 flutter_screenutil,而是把 fork 过的 flutter_screenutil、全局初始化策略,以及页面里的实际写法串成一套可复用的工程方法。整体思路也参考了 SwiftyFitsize 的做法。
一句话概括这套方案:
保留
375设计稿作为统一基准,在全局缩放层抑制大屏放大,在页面结构层按大屏分支,在局部尺寸层通过.fw、LayoutBuilder、真实列宽来修正宽屏失真。
1. 先说结论:这套方案是"三层适配"
折叠屏 / 宽屏适配,不是靠单一技巧完成的,而是三层一起工作:
- 全局缩放层 :fork
flutter_screenutil,引入大屏宽度系数,避免展开后所有控件一起变胖。 - 结构分支层 :用
ScreenUtil().isLargeScreen切换列数、横滑 / 铺平、单双栏等布局结构。 - 局部修正层 :针对 banner、地图卡片、Grid、AppBar、底部占位等高频问题,用
.fw、LayoutBuilder、getter 重算等方式定点修复。
这三层的关系可以理解为:
- 第一层解决"整体太大"
- 第二层解决"结构不合理"
- 第三层解决"具体组件看起来还是不对"
2. 为什么选择 fork flutter_screenutil
这类工程通常不会放弃 screenutil,反而会继续把它当作全局尺寸基准,只是在宽屏场景下补齐原库不够用的能力。
常见做法是:
yaml
dependency_overrides:
flutter_screenutil:
git:
url: https://github.com/zeqinjie/flutter_screenutil.git
ref: master
再由基础库统一对外导出:
dart
export 'package:flutter_screenutil/flutter_screenutil.dart';
这意味着:
- 业务代码仍沿用熟悉的
.w / .h / .sp - 适配能力可以通过基础库统一下发
- 不需要每个业务模块各自发明一套宽屏规则
这其实很重要。宽屏适配最怕的不是某个页面不好修,而是不同人修出不同口径,最后工程里同时存在三四套断点与尺寸语义。
3. fork 版 flutter_screenutil 到底加了什么
这次 fork 最关键的不是新增几个 API,而是把"手机尺寸语义"和"大屏尺寸语义"拆开了。
3.1 大屏判定
常见的新增能力是:
ScreenUtil().isLargeScreenlargeScreenShortestSideBreakpoint
通常按 最短边 >= 600 判定为大屏,这和业务里的布局分支保持一致。
这带来的直接收益是:
- 页面做大屏分支时统一写
ScreenUtil().isLargeScreen - 避免手写
MediaQuery.shortestSide >= 600 - 以后如果调整断点,业务代码和尺寸缩放仍能保持同一口径
3.2 大屏宽度系数
在 ScreenUtilInit 和 ScreenUtil 中新增:
largeScreenFitMultiplelargeScreenWidthFactor
一个典型初始化如下:
dart
return ScreenUtilInit(
designSize: const Size(375, 667),
minTextAdapt: true,
splitScreenMode: true,
useInheritedMediaQuery: true,
largeScreenFitMultiple: 0.5,
largeScreenShortestSideBreakpoint: 600,
builder: ...
);
这里最核心的是 largeScreenFitMultiple: 0.5。
它的含义不是"把整个页面缩小到一半",而是:
- 仍然按设计稿宽度比缩放
- 但在大屏场景下,再给宽度链路乘一个额外系数
- 从而抑制折叠展开态 / 平板下控件、留白、字号随宽度线性放大的问题
换句话说,这套方案并没有改掉 375 设计基准,而是在大屏下让"宽度放大曲线"变得更克制。
3.3 强制宽度链:.fw / .fsp
fork 版还会补两组很关键的扩展:
.fw:按原始宽度比例缩放,不乘大屏系数.fsp:字体按原始宽度比例缩放,不乘大屏系数
以及对应的:
.fh.fr.fdg.fdm
但从真实落地经验来看,最常用、最值得记住的还是:
- 默认用
.w / .sp - 少数横向尺寸用
.fw
4. 这套方案里的真正约定
这套方案并不是"看到大屏就全部改成 .fw",而是已经形成了一套比较稳定的约定。
4.1 结构变化用 ScreenUtil().isLargeScreen
凡是下面这类场景,都优先用 ScreenUtil().isLargeScreen 处理:
- 手机 2 列,大屏 3 列
- 手机横滑,大屏铺平
- 手机单列,大屏双列
- 地图 / 侧栏等场景切换容器宽度
例如:
dart
final int crossAxisCount = ScreenUtil().isLargeScreen ? 3 : 2;
这类写法表达的是"布局结构变了",而不是"同一个宽度单位换了算法"。
4.2 默认继续使用 .w / .sp
主路径仍然是:
- 高度用
.w - 竖向间距用
.w - 大多数字号继续用
.sp - 普通圆角、边框、纵向节奏继续沿用原有写法
这是因为全局已经有 largeScreenFitMultiple 帮我们兜住"大屏整体过大"的问题,绝大部分组件不需要再做额外动作。
4.3 只有横向尺寸需要"保留设计宽感"时,才改成 .fw
.fw 更像一把手术刀,而不是全量替换方案。
典型场景包括:
- 地图卡片固定设计宽
- 横向弹层固定设计宽
- 横向 banner 高度不变,但宽度不要被大屏再放大
- 某些需要保持手机设计观感的列表卡片宽度
例如:
dart
SizedBox(width: 375.fw);
dart
final double topViewWidth =
ScreenUtil().isLargeScreen ? 364.fw : 355.w;
dart
Container(width: 343.fw);
dart
SizedBox(height: 48.fw);
这背后的经验可以总结成一句话:
当你想抵消大屏额外宽度系数时,只改横向相关尺寸,不要顺手把整块组件的高度、竖向间距也一起改掉。
4.4 不要写 isLargeScreen ? n.fw : n.w
这是这套方案里一个很容易反复出现的伪分支。
在这类 fork 实现里,非大屏时 .fw 和 .w 数值本来就是一样的。所以如果某个尺寸的设计意图就是"横向不要乘大屏系数",那直接写:
dart
343.fw
就够了。
不要再额外写:
dart
ScreenUtil().isLargeScreen ? 343.fw : 343.w
这会让代码显得更复杂,但没有带来额外语义。
4.5 状态对象里,和屏幕相关的尺寸尽量写成 getter
这个点很细,但很关键。
在折叠态、展开态、分屏态之间切换时,如果尺寸是在对象构造期就算死的,就会出现:
- 底部留白不对
- 避让高度不对
- 弹层卡住
- 列表内容与真实底栏高度不一致
因此更稳妥的做法是:随屏幕变化的尺寸用 getter,每次读取时重算。
例如:
dart
double get bottomContactHeight => 60.w;
这类写法比 final double bottomContactHeight = 60.w; 更适合折叠屏场景。
4.6 AppBar 高度要和 title 区域一起适配
自定义 AppBar 在大屏里很容易出现一个经典问题:
title根容器已经按.w放大- 但
toolbarHeight还停留在默认值 - 结果就是标题被裁切,或者与返回按钮垂直不对齐
比较稳的写法是:
toolbarHeight跟随适配PreferredSizeWidget.preferredSize也同步适配
例如:
dart
@override
Size get preferredSize => Size.fromHeight(kToolbarHeight.w);
5. 页面级适配里,最常见的四种修法
如果把宽屏问题归类,真正高频的其实就四种。
5.1 Grid / 瀑布流:列数分支 + 真实列宽重算比例
这是折叠屏适配里最常见的一类问题。
表面看是:
- Grid 裁切
- 某一行被挤爆
- 大屏下格子变得又高又空
本质原因通常是:
- 列数变了
- 列宽变了
childAspectRatio却还是旧值- 或者外层高度仍然是过去按手机宽度拍脑袋写死的
比较成熟的写法,是把 LayoutBuilder 和真实列宽结合起来。
这里不是写死 childAspectRatio: 1,而是:
- 通过
LayoutBuilder拿到实际可用宽度 - 计算
cellWidth - 用设计稿下的参考行高推导
childAspectRatio
例如:
dart
typedef GridAspectMetrics = ({
double cellWidth,
double referenceRowHeight,
double childAspectRatio,
});
GridAspectMetrics computeGridAspectMetrics({
required double gridInnerMaxWidth,
required int crossAxisCount,
double designWidth = 375,
double designHorizontalPadding = 16 * 2,
}) {
final double cellWidth = gridInnerMaxWidth / crossAxisCount;
final double referenceRowHeight =
((designWidth - designHorizontalPadding) / crossAxisCount).w;
final double childAspectRatio = cellWidth / referenceRowHeight;
return (
cellWidth: cellWidth,
referenceRowHeight: referenceRowHeight,
childAspectRatio: childAspectRatio,
);
}
也就是:
列宽跟着真实空间走,行高保持设计稿节奏,最终比例动态算出来
这样在手机、折叠展开、平板上,宫格都会更稳定。
5.2 横滑列表 / 地图卡片:固定设计宽走 .fw
地图、横滑卡片、底部拖拽卡通常都很依赖"视觉宽度感"。
如果这些组件继续走默认 .w,在大屏下会被额外放大,看起来容易出现:
- 卡片太胖
- 文字太散
- 与地图、遮罩、操作区比例失衡
所以这类区域里常见写法会是:
375.fw343.fw364.fw
这类写法的本质不是"适配大屏",而是"在大屏下保留手机设计稿里的横向控制感"。
5.3 banner / 横向入口:横向压住,纵向节奏保持原样
banner 是最典型的"只改宽,不动高"的区域。
例如:
dart
CarouselOptions(
viewportFraction: 1.0,
height: 48.fw,
)
虽然这里用了 fw,但它表达的仍然是"横向观感按设计稿控制",并不是鼓励把整块组件所有尺寸都改成 force 版本。
在实际经验里,banner 这类区域通常遵循:
- 横向尺寸按需
.fw - 纵向视觉节奏尽量保留原设计
- 如果背景图需要铺满,再配合
fitWidth、cover或fill做局部修正
5.4 大屏结构改造优先于魔法数微调
这类问题经常说明一件事:
当问题来自结构本身时,最有效的办法不是继续调尺寸,而是直接换结构。
例如:
- 手机横滑,大屏改成多列铺平
- 手机 2 列,大屏 3 列
- 单行横向入口,大屏改成更稳定的
Row/Expanded结构
所以遇到折叠屏问题时,排查顺序建议是:
- 先看是不是应该换结构
- 再看是不是要改
.fw - 最后才是微调某几个 magic number
6. 这套方案为什么比"改 designSize"更稳
很多工程遇到折叠屏,第一反应是:
- 大屏时把
designSize改大 - 或者单独给平板一套设计稿
但这通常不是主路径,原因也很现实:
- 页面存量很多
.w / .sp的心智已经深度绑定375设计稿- 动态切
designSize会放大排查成本 - 组件库、业务库、宿主工程之间容易出现缩放语义不一致
相较之下,这套方案的优势是:
- 延续旧心智 :大家依然围绕
375写页面 - 全局改动小:集中在 fork 包和宿主初始化
- 业务接入渐进:有问题的页面按需治理
- 语义明确 :
.w是默认链路,.fw是大屏横向逃生口
7. 一份可以直接带走的团队约定
如果要把这套方案浓缩成团队规范,可以这样写:
全局层
- 统一使用 fork 版
flutter_screenutil - 保持
designSize: Size(375, 667) - 通过
largeScreenFitMultiple控制大屏宽度缩放曲线 - 结构断点统一走
ScreenUtil().isLargeScreen
页面层
- 布局结构变化优先用大屏分支处理
- Grid / 瀑布流优先用
LayoutBuilder+ 真实列宽重算比例 - 横向固定设计宽的卡片、弹层、入口视图按需使用
.fw - 不要滥用
.fw,尤其不要把竖向节奏一起 force 掉
编码层
- 不写
isLargeScreen ? n.fw : n.w - 屏幕相关占位高度优先写 getter
- 自定义 AppBar 的
toolbarHeight、title 高度、preferredSize保持一致
8. 最后的经验总结
这套折叠屏 / 宽屏适配方案,本质上不是在追求"所有页面在所有设备上一模一样",而是在做一件更现实的事:
用尽量小的全局改动,换取尽量稳定的宽屏体验,并给业务页面留下足够明确、足够统一的修正手段。
它最有价值的地方,不是新增了 .fw 或 isLargeScreen,而是把适配思路收敛成了统一语言:
- 全局靠
largeScreenFitMultiple - 结构靠
isLargeScreen - 局部横向靠
.fw - 比例问题靠
LayoutBuilder - 动态占位靠 getter 重算
当团队都按这套语言协作时,折叠屏适配就不再是"某个页面的特殊修补",而会变成一套可维护、可推广、可持续复用的工程能力。
9. 使用
适配前

适配后

use
yaml
flutter_screenutil:
git:
url: https://github.com/zeqinjie/flutter_screenutil.git
ref: master