Flutter 内置布局组件:适用场景与边界

底部操作栏的按钮要贴右、标签过多要换行、卡片要占父容器 80% 宽度、长文本小屏上别溢出。这些需求写 Flutter 一两个月就会撞见,而我最初的实现方式全是手算:MediaQuery 拿宽度、嵌套 Container 做对齐、拆多个 Row 模拟换行。代码能跑,但每处都残留着“设备相关”的隐患。
后来系统翻了一遍内置组件,发现这些问题框架早就封装好了。下面这 10 个组件,我按使用场景记录,每个都标了适用前提和边界条件,不是单纯罗列 API。
1. Spacer
解决的问题:在 Row/Column 中,把某个元素推到主轴的另一端,比如导航栏右侧的按钮。
前提:父级必须是 Flex(Row、Column、Flex),且主轴上还有未被占用的空间。Spacer 本质是 Expanded 套一个空 SizedBox,所以它遵循 flex 分配规则。
适用:导航栏、个人资料页头部、操作按钮行。
Row(
children: [
Icon(Icons.menu),
Spacer(),
Icon(Icons.search),
Icon(Icons.more_vert),
],
)
不适用 / 取舍:
- 需要按比例分割多个空隙时,直接给
Spacer传flex参数,或者用MainAxisAlignment.spaceBetween,后者语义更清楚。 - 主轴约束无限时(比如在滚动视图中沿滚动方向),
Spacer没有空间可分,会失效。 - 如果需要在剩余空间里放真实内容,用
Expanded而不是Spacer。
2. Wrap
解决的问题:Row 的默认假设是“所有子组件都能水平放下”,放不下就报溢出。Wrap 会在主轴占满后自动换行。
前提:子组件数量或宽度不确定,且换行本身是合理的交互。空间不足时,Wrap 按顺序把子组件排到下一行。
适用:标签列表、筛选条件、Chips 组、搜索历史。
Wrap(
spacing: 8,
runSpacing: 8,
children: List.generate(10, (i) => Chip(label: Text('标签 $i'))),
)
不适用 / 取舍:
Wrap不是网格布局。想要固定列数、对齐整齐的网格,用GridView。Wrap一次性布局所有 child,没有懒加载。子项数量大且复用复杂组件时,考虑ListView。- 记得设置
spacing和runSpacing,否则子组件会贴在一起。
3. OverflowBar
解决的问题:一组操作按钮,空间足够时水平排列,空间不足时整体切换为垂直排列,避免溢出。
前提:子项数量有限,并且你能接受“整体切换方向”的行为——它不是逐个子项换行,而是要么横排、要么竖排。
适用:对话框底部按钮、详情页操作按钮组。
OverflowBar(
alignment: OverflowBarAlignment.end,
children: [
TextButton(onPressed: () {}, child: Text('取消')),
ElevatedButton(onPressed: () {}, child: Text('确认')),
],
)
不适用 / 取舍:
- 想逐项换行时用
Wrap。 - 切换阈值由子项总宽和
spacing间接决定,不能指定显式断点。需要精确控制宽屏/窄屏行为时,LayoutBuilder更直观。 - 它只处理方向切换,不包括按钮间的复杂间距和分组逻辑。
4. Align
解决的问题:把单个子组件放到父容器的指定位置,比如右上角、左下角,不需要为了一次对齐去包一层 Container。
前提:只有一个 child,且定位范围是 Align 自身收到的约束。
适用:角标、浮动标签、叠加式 UI 的小元素。
Align(
alignment: Alignment.topRight,
child: Badge(label: Text('新')),
)
不适用 / 取舍:
- 多个子组件叠加时用
Stack,不是Align。 Alignment的坐标是 −1 到 1 的因子(0 表示居中),不是像素也不是直接百分比。需要像素偏移时,外层加Padding或改用Positioned。- 想让 child 铺满整个可用空间时,用
SizedBox.expand而不是Align。
5. FractionallySizedBox
解决的问题:让子组件占据父容器宽度(或高度)的一定比例,不需要自己拿 MediaQuery 计算屏幕宽度再乘系数。
前提:父容器必须提供有界约束。比例是基于约束的 max 计算的,所以放在 ListView 主轴方向、UnconstrainedBox 这类无限约束环境中无法生效。
适用:卡片占父宽度 80%、进度条高度比例、响应式内容区。
FractionallySizedBox(
widthFactor: 0.8, // 占据父宽度的 80%
child: Card(child: ...),
)
不适用 / 取舍:
- 它只控制尺寸,不负责对齐。占比之后要居中,还得再套一层
Align或Center。 - 父容器尺寸依赖自身内容时,比例计算会变得别扭,这时用
AspectRatio或固定尺寸更直接。 - “永远铺满”的场景用
SizedBox.expand,不需要FractionallySizedBox。
6. FittedBox
解决的问题:子组件在可用空间内放不下时,按比例缩放来避免溢出。
前提:子组件有固有尺寸且可等比缩放(文本、图标、图片),父约束有界。fit: BoxFit.scaleDown 表示只在溢出时缩小,不放大。
适用:单行长文本的兜底、把大尺寸元素塞进固定小容器。
FittedBox(
fit: BoxFit.scaleDown,
child: Text('一段很长的文本内容', style: TextStyle(fontSize: 24)),
)
不适用 / 取舍:
- 正文长文本慎用。无限缩小会明显损害可读性,优先考虑
Text的overflow和maxLines。 FittedBox是绘制缩放,缩放后外层布局尺寸可能仍然大于视觉可见区域,和相邻组件排版时容易出现“看起来有空隙”的情况。- 文本、图片之外的复杂组件用
FittedBox,效果往往不好预测。
7. LayoutBuilder
解决的问题:布局依赖“当前组件实际接收到的约束”,而不是整块屏幕的尺寸。同一套组件放在侧边栏和全屏页里,可以根据容器宽度自动切换布局。
前提:你有清晰的分支条件,比如 maxWidth > 600 用宽布局,否则用窄布局。约束信息由父级传递,LayoutBuilder 不做任何测量之外的推断。
适用:可复用组件、分栏布局、容器内自适应布局。
LayoutBuilder(
builder: (context, constraints) {
if (constraints.maxWidth > 600) {
return _buildWideLayout();
} else {
return _buildNarrowLayout();
}
},
)
不适用 / 取舍:
- 全局响应式需求(整个页面随屏幕尺寸变化)用
MediaQuery更直接,不必每个组件都套LayoutBuilder。 - 父级约束频繁变化时,
builder会跟着重建,注意这里的额外 build 开销。 - 它只给你约束,不帮你在两个布局之间做动画过渡。切换效果需要自己处理。
8. LimitedBox
解决的问题:在无限约束的环境里(如滚动视图的主轴方向),给子组件一个最大尺寸兜底,防止它无限扩张或布局崩溃。
前提:近邻父级确实传入了无限约束。如果约束本身是有界的,LimitedBox 直接忽略——它更像“默认值”,不是强制限制。
适用:滚动列表里的子项默认最大高度、自定义方向布局中的兜底尺寸。
LimitedBox(
maxHeight: 200,
child: ListView(children: [...]),
)
不适用 / 取舍:
- 需要真正的强制约束时用
ConstrainedBox。 LimitedBox在有界约束下不生效,容易让人误以为“设置了但不起作用”。确认使用场景时先看父约束是不是无限。- 它只限制传入方向的尺寸,不会对内容做缩放或裁剪。
9. IntrinsicHeight
解决的问题:让 Row 里高度不一的兄弟组件等高,以最高的那个为准。
前提:子组件高度需要互相参照,通常搭配 crossAxisAlignment: CrossAxisAlignment.stretch 使用。
适用:两栏卡片等高、操作区域内的横向对齐。
IntrinsicHeight(
child: Row(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Expanded(child: Container(color: Colors.red, child: Text('短内容'))),
Expanded(child: Container(color: Colors.blue, child: Text('长内容\n第二行'))),
],
),
)
不适用 / 取舍:
- 官方文档将其标注为 expensive:每个子组件都要做一次内在尺寸测量,布局成本比普通组件高。列表项、大列表、频繁重建的区域不要用。
- 如果高度可以通过固定值、
AspectRatio或父容器约束直接确定,就别用IntrinsicHeight。 - 它只在高度方向上做内在测量,水平方向对应的是
IntrinsicWidth,两者机制相同、开销也相同。
10. SliverFillRemaining
解决的问题:在 CustomScrollView 中,让内容填满视口剩余空间,典型场景是空状态页、登录引导页。
前提:它必须是 sliver,只能作为 CustomScrollView 的 slivers 成员使用,放在普通 Column 里无效。
适用:SliverAppBar 之后需要把内容推到视口底部或居中的布局。
CustomScrollView(
slivers: [
SliverAppBar(title: Text('标题')),
SliverFillRemaining(
child: Center(child: Text('这是剩余空间的全部内容')),
),
],
)
不适用 / 取舍:
- 普通
Column场景下想填满整屏,用ConstrainedBox(minHeight: viewportHeight)更简单。 - 它依赖 sliver 的约束模型。内容超过剩余空间时,是否允许滚动需要额外配置(
hasScrollBody),这是原文未提供的细节,实现前建议翻一下文档。 - 组合
SliverAppBar时,剩余空间的计算自动发生,比自己手动量高度干净得多。
什么情况下值得回来翻组件表
我自己的判断信号:
- 开始手写
宽 = screenWidth * 0.8这类算式; - 为了对齐包了一层什么都不干的
Container; - 用多个
Row手动拼“换行”; - 自己写溢出检测或测量逻辑。
出现这些信号,先查一遍内置组件,再考虑第三方库。组件本身没有高下之分,关键是哪个能最直接地表达布局意图——内置组件多数时候就是答案。