Flutter - 布局问题

7 阅读5分钟

我用装修房子+生活场景的大白话给你讲透,保证你看完再也不会忘,写代码也不会踩坑!


一、先记住Flutter布局的唯一 黄金法则(一句话)

**爸爸(父Widget)给儿子(子Widget)定规矩(约束):你只能在这个范围内长;

儿子告诉爸爸:我实际长这么大(尺寸)。**

  • 约束:从上往下传(父→子)
  • 尺寸:从下往上传(子→父)
  • 核心:父级说了算范围,子级说了算自己具体多大

二、三种约束分别是什么?(人话版)

1. 紧约束(Tight Constraints)

人话翻译:必须刚好这么大,一点都不能变!

  • 父级给子级:最小宽=最大宽最小高=最大高
  • 子级:没有任何选择空间,必须严格按父级给的尺寸渲染

生活例子:

你定制了一个嵌入式 衣柜,刚好卡进墙里的凹槽:

  • 凹槽宽1米、高2米(紧约束)
  • 衣柜必须刚好宽1米、高2米,不能大也不能小

代码例子:

SizedBox(
  width: 100, // 紧约束:必须100宽
  height: 100, // 紧约束:必须100高
  child: Container(color: Colors.blue), // Container只能是100x100
)

2. 松约束(Loose Constraints)

人话翻译:别超过这个范围,你想多大就多大!

  • 父级给子级:最小宽=0最小高=0,只给最大宽高
  • 子级:可以选0到最大值之间的任意尺寸

生活例子:

你给孩子一个墙角放玩具箱

  • 墙角最大能放1米宽、1米高的箱子(松约束)
  • 孩子可以放一个50cm的小箱子,也可以放1米的大箱子,只要不超过1米就行

代码例子:

Center( // Center给子级松约束
  child: Container(
    width: 50, // Container自己选50宽,不用填满Center
    height: 50,
    color: Colors.blue,
  ),
)

3. 无边界约束(Unbounded Constraints)

人话翻译:你想多大就多大,我不管!(但这是个坑)

  • 父级给子级:最大宽/高 = 无限大(double.infinity)
  • 子级:可以无限延伸

生活例子:

你在无限长的走廊里铺地毯:

  • 走廊没有尽头(无边界约束)
  • 地毯可以一直铺下去,想铺多长铺多长

代码例子:

ListView( // ListView在滚动方向给子级无边界约束
  children: [
    // 子Item可以无限延伸
  ],
)

三、最常见的异常:viewport has unbounded height 到底啥意思?

人话翻译:

“无限大的爸爸”遇到了“想填满爸爸的儿子”,形成了死循环!

核心根因:

  1. 父级(比如ListView、SingleChildScrollView)给了子级无边界约束(“你想长多长长多长”)
  2. 子级(比如另一个ListView、Column)又想填满父级的所有空间(“爸爸多大我多大”)
  3. Flutter懵了:“到底是无限大?还是要填满?我算不出来尺寸了!”
  4. 于是抛出异常。

最常见的3个踩坑场景+解决办法(人话版)

场景1:Column里直接放ListView(90%的人都踩过)

踩坑代码:
Column( // Column是固定高度的(紧约束)
  children: [
    Text('标题'),
    ListView( // ListView在垂直方向是无边界约束
      children: [...],
    ),
  ],
)
为什么报错?
  • Column:“我高度固定,你(ListView)告诉我你要多高?”
  • ListView:“我是无限长的,你(Column)给我多少空间我就占多少!”
  • 死循环 → 报错
解决办法(2选1):
  1. 给ListView定个固定高度

      Column(
        children: [
          Text('标题'),
          SizedBox(
            height: 300, // 定死高度:你就300高,别无限长了
            child: ListView(...),
          ),
        ],
      )
      ```
    
    
  2. Expanded 包裹ListView(推荐):

      Column(
        children: [
          Text('标题'),
          Expanded( // 意思:Column剩下的空间全给你
            child: ListView(...),
          ),
        ],
      )
      ```
    

场景2:SingleChildScrollView里嵌套ListView

踩坑代码:
SingleChildScrollView( // 父级:垂直方向无边界约束
  child: ListView( // 子级:垂直方向也是无边界约束
    children: [...],
  ),
)
为什么报错?
  • 两个都是“无限长”,Flutter不知道到底听谁的 → 报错
解决办法:

让ListView别自己滚动了,高度根据内容自适应,跟着父级一起滚:

SingleChildScrollView(
  child: ListView(
    shrinkWrap: true, // 关键:ListView高度根据子Item总高度自适应
    physics: NeverScrollableScrollPhysics(), // 关键:关闭ListView自己的滚动
    children: [...],
  ),
)

场景3:自定义RenderObject里用了无限大尺寸

踩坑代码:
// 自定义布局里,给子级传了无限约束,子级又用double.infinity设置尺寸
// 死循环 → 报错
解决办法:

自定义布局时,先判断是不是无边界约束,如果是,给子级设个合理的上限,别真的让它无限大。


四、复杂自定义布局的性能优化(人话版)

核心就是一句话:别做重复工作,别算没用的东西

  1. 少嵌套:别一层套一层Row/Column/Stack,能用一个Widget实现的,别拆成好几个。

    1. 就像装修:能直接在墙上挂画的,别先装个架子再挂画。
  2. 别每次都重新算布局:布局约束没变的话,就用上次算好的结果,别每帧都重新算一遍。

  3. 只算看得见的:长列表里,只算屏幕里能看到的Item,看不见的先不算。

  4. 提前算好静态的:固定不变的尺寸,提前算好存起来,别每次build/layout都重新算。


五、终极极简总结

  1. 黄金法则:父级给范围(约束),子级给尺寸。

  2. 三种约束

    1. 紧约束:必须刚好这么大
    2. 松约束:别超范围,随便多大
    3. 无边界约束:想多大多大(容易踩坑)
  3. 常见异常:无限大的父级遇到想填满的子级 → 死循环

  4. 解决办法:定高度、用Expanded、让子级自适应

  5. 性能优化:少嵌套、别重复算、只算看得见的