Flutterのレイアウトを理解して極める

# はじめに
Flutterを使った開発で、よく遭遇するであろうエラーに
`Vertical viewport was given unbounded height.`
`A RenderFlex overflowed by ~ pixels on the bottom.`
などがあります。今回はこれに関わるものとして、
①レイアウトの簡単な仕組みと具体例
②レイアウトの詳細
③実際のサンプル問題を通して理解を深める
の3つを書いていきたいと思います。

# 対象読者
- Flutter初学者
- 上のエラー文で検索してきた人
- ③のサンプル問題が解けなかった人

# 参考
https://docs.flutter.dev/development/ui/layout/constraints

# ①レイアウトの簡単な仕組みと具体例
Flutterのレイアウトは上の参考記事にある通り、`Constraints go down. Sizes go up.`が全てです。 (positionは特に触れないため今回の記事から省きました)
上を図にすると、

まず、親が子にconstraints(制約)を伝える。
```mermaid
graph LR
    A[親Widget]-- constraints -->B[子Widget]
```

それを踏まえて、子は親に自分のsizesを伝える。
```mermaid
graph LR
    A[子Widget with constraints]-- sizes -->B[親Widget]
```

のようになっています。

具体的な数字を入れてみるとこんな感じです。
まず、親が子にconstraints(制約)を伝える。
```mermaid
graph LR
    A[親Widget]-- 0以上100以内のwidthにしてね -->B[子Widget]
```

それを踏まえて、子は親に自分のsizesを伝える。
```mermaid
graph LR
    A[子Widget with Constraints]-- じゃあ50にします -->B[親Widget]
```

エラーは、**sizesがconstraintsを何かしらの理由で超えてしまった場合**や**予期せぬconstraintsを親から受け取った場合**などに生じます。
また、Flutterはツリー構造なので、孫widgetも登場します

```mermaid
graph LR
    A[親Widget]---B[子Widget]---C[孫Widget]
```

この場合は、constraintsは孫Widgetにまで渡ってから、次は逆にsizesを親Widgetまで伝えます。

さらに、子Widgetが複数存在することもあります。
```mermaid
graph LR
    A[親Widget]---B[子Widget]
    A[親Widget]---C[子Widget]
    A[親Widget]---D[子Widget]
```

こうなってくると、子は親のconstraintsに収まるのが難しくなりそうですよね...

## 具体例
では実際に具体例を見てみましょう。今はwidthだけを考えていきます。なお、ウィンドウ幅は500であるとしてください。

```dart
return Container(
  width: 100,
  color: Colors.red,
  child: SizedBox(
    width: 150,
  ),
);
```

これは親Widgetが`Container`、子Widgetが`SizedBox`という関係になっています。
**読み進める前に、上のソースコードを描画するとどのような結果になるのか予想してみてください。**

## Constraintsについて

まず、constraintsから考えていきます。
`Container`は子にどんな制約を持つでしょうか？
`Container`はconstraintsに何も指定しない場合、`BoxConstraints.tightFor`が設定されます。
すると、これは「子を100widthにしてくれ」という制約になります。

続いて、`SizedBox`が子に持つ制約を考えます。
`SizedBox`も同じく、`BoxConstraints.tightFor`のconstraintsが使われています。よって、「子を150widthにしてくれ」という制約があるのですが、`SizedBox`や`Container`は**自分のconstraintsを親のconstraintsの範囲に収める**という特徴があります。すると、ここでも「子を100widthにしてくれ」という制約になります。

**実はもう一つ考えるべきconstraintsがあります。** それは[RenderView](https://api.flutter.dev/flutter/rendering/RenderView-class.html)の存在です。
Flutterは`RenderView`をルートに持つので、このconstraintsも考えておく必要があります。[ソースコード](https://github.com/flutter/flutter/blob/75ee440329f555cac3d636a24bdc5c16c8ef60ef/packages/flutter/lib/src/rendering/view.dart#L170)によると、ここでは、`BoxConstraints.tight`が使われているようです。デバイスの幅が500とすると、これは「子を500widthにしてくれ」という制約になります。
すると、`Container`や`SizedBox`の制約も変わってきますね。

ここで、constraintsに関してまとめておきます。
### constraints
| RenderView | Container | SizedBox |
| --- | --- | --- |
| 子を500width | 子を500width | 子を500width |

結局全て同じconstraintsになりました。
また、今回は簡単のためheightを考えませんでしたが同じことです。

## sizesについて
次にようやく、sizesを考えることができます。
sizesを考える場合は、子から親方向へ考えていきます。
ではまず`SizedBox`からです。`SizedBox`は子を持つ場合と持たない場合で、サイズの指定が変わります。子を持たない場合、[ソースコード](https://github.com/flutter/flutter/blob/75ee440329f555cac3d636a24bdc5c16c8ef60ef/packages/flutter/lib/src/rendering/proxy_box.dart#L295)によるとconstraintsに収まる範囲で、0に近くなるようにしてサイズが決まるようです。よって、今回は500widthピッタリです。

`Container`は子がある場合は、子のサイズがそのまま自分のサイズになるようです。よって同じく500widthです。

ここでsizesについてまとめます。
### sizes
| Container | SizedBox |
| --- | --- |
| 500width | 500width |

結局これも500となりました。

## 描画結果
ソースコードの描画結果を下に示します。
![Screen Shot 2022-08-20 at 12.24.00 AM.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/372622/6ccbbfce-1b32-89b1-1aaa-9df892c6ae7a.png)

全画面が真っ赤になっていますね。
これは全てのconstraintsが結局ウィンドウサイズとなり、全てのsizesもウィンドウサイズとなっているからです。
また、**子のwidthが親のwidthよりも大きいにも関わらず、エラーが出ていない**ことは意外ではないでしょうか？これは本当のwidthはconstraintsとsizesの関係で決まることを意味しています。(設定した150がそのまま使われる訳ではない)

## Scaffoldを使う
ここで、上のソースコードを少し修正してみます。
```dart
return Scaffold( //**** 変更箇所 ****//
  body: Container(
    width: 100,
    color: Colors.red,
    child: SizedBox(
      width: 150,
    ),
  ),
);
```
`Scaffold`を`Container`の親に置いてみました。
これによって、どのようにconstraintsが変わるのか確認します。
[ソースコード](https://github.com/flutter/flutter/blob/6e66f064f8ed2d6fa60f33a5db3f8b3e12c5471f/packages/flutter/lib/src/material/scaffold.dart#L1067)によると、`Scaffold`は**親のconstraintsを子のconstraintsへ伝える方法**が`SizedBox`や`Container`とは異なります。widthのconstraintsに関しては、`minWidth:0, maxWidth:親のconstraintsの最大幅`のようになります。高さのconstraintsに関しても、親のconstraintsの最大高さからappBarなどのコンテンツの高さを引いて求めています。つまり、必ずしも親のconstraintsの中に収めるのではなくて、親のconstraintsの最大値以下の範囲に収めています。(最小値は親のconstraintsを下回ることが可能)

最終的に全体のconstraintsは次のようになります。
今回はウィンドウ高さは1000としてください。
| RenderView | Scaffold | Container (width 100) | SizedBox (width 150) |
| --- | --- | --- | --- |
| minWidth:500 maxWidth:500 minHeight:1000 maxHeight:1000 | minWidth:0 maxWidth:500 minHeight:0 maxHeight:1000 | minWidth:100 maxWidth:100 minHeight:0 maxHeight:1000 | minWidth:100 maxWidth:100 minHeight:0 maxHeight:1000 |

**`Container`や`SizedBox`のmin-maxWidthが500に固定されなくなりました!**
(今回は特に`appBar`など指定していないのでウィンドウ高さの1000が`Scaffold`でそのまま利用される形となってます)

今度はsizesを考えます。
`Scaffold`のサイズは[ソースコード](https://github.com/flutter/flutter/blob/6e66f064f8ed2d6fa60f33a5db3f8b3e12c5471f/packages/flutter/lib/src/rendering/custom_layout.dart#L409)によると、constraintsの最大値でchildのサイズに寄らないのが特徴です。
他も1つずつ考えていくと、以下の表のようになります。
| Scaffold | Container | SizedBox |
| --- | --- | --- |
| width:500 height:1000 | width:100 height:0 | width:100 height:0 |

描画結果は以下のようになります。
実際は`Container`などは描画されているんですが、高さが0になっているので見えなくなっています。
(`SizedBox`もしくは`Container`に`height`を設定してあげると、赤の矩形が見えるようになります。)

![Screen Shot 2022-08-20 at 1.17.56 AM.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/372622/19d1ba28-78c3-c091-801e-c52ff93117b3.png)

## ここまでのまとめ
だいぶややこしくなってきました。一旦ここまでの話をまとめたいと思います。
親widgetをA、子widgetをB、孫widgetをCとします。
- constraintsはAからBそしてCへ伝わる
- AからBに伝わったconstraintsはBからCにそのまま伝わる訳ではない。
- constraintsは基本minWidth、maxWidth、minHeight、maxHeightの4つで構成される
- sizesは基本的にCからBそしてAへ伝わる
    - ただし、`Scaffold`のように子のsizesは関係なしに、自身のsizesを決めるものがある

# ②レイアウトの詳細
①で見たように、constraintsや、sizesの伝わり方、決め方に一定のルールはあるものの、widgetごとに微妙に挙動が違うことがあります。

## constraintsについて
### constraintsの種類
まずはconstraintsの基本である、`BoxConstraints`にどんなコンストラクタがあるか見てみます。これを見ることで、min-maxWidth、min-maxHeightにどんな値が入るのか理解できるようになります。
コンストラクタの種類は以下です。

| コンストラクタ名 | 説明 |
| --- | --- |
| デフォルト | 引数がなければ0~∞。あればその値。 |
| tight | 1つの同じ値をmin-maxに指定する。よって範囲はない。 |
| tightFor | 引数がnullのときは0~∞。それ以外の引数ならtightと同じ。 |
| tightForFinite | 引数が∞のときは0~∞。それ以外の引数ならtightと同じ。 |
| loose | minは0で固定されており、maxを指定する。 |
| expand | 引数がnullの時は∞。それ以外の引数ならtightと同じ。 |

`tightFor`は`Container`や`SizedBox`で使われていましたね。
こうしてみてみると、デフォルトを除いて、looseだけが少し仲間はずれなパターンです。他は引数が条件を満たせば、`tight`と全く同じ挙動です。
例外として、`_BodyBoxConstraints`という`BoxConstraints`を継承したクラスがあります。これは`Scaffold`のbody専用で、おおよそlooseと同じ挙動です。

### `BoxConstraints`のメソッド
①で最後まとめたように、親から子に伝わったconstraintsはそのまま孫に伝えていくことは少なく、少し変更されてから伝わると書きました
どんなマージ方法があるのか、今度はメソッドを見てみましょう。

| メソッド名 | 説明 |
| --- | --- |
| enforce | 引数のconstraintsの範囲に収まりかつ、元々のconstraintsとなるべく近くなる形で、新たな`BoxConstraint`を返す |

constraintsを引数にとるメソッドは1つだけでした。
`Container`や`SizedBox`はこのメソッドを使っているので、①で書いたように親のconstraintsに収まるようにconstraintsが変更されていました。

`Container`の場合、多くの記事で
> 子を持たない場合、可能な限り大きく表示される

のように書かれていることがあります。ただこれは、あまり厳密ではなく片手落ちです。
[Container](https://api.flutter.dev/flutter/widgets/Container-class.html)のドキュメントにもある通り、厳密にはこうです。
> Containers with no children try to be as big as possible unless the incoming constraints are unbounded, in which case they try to be as small as possible.

> 子を持たないコンテナーは、入ってくる制約が無制限でない限り、できるだけ大きくしようとします。制限がない場合は、できる限り小さくしようとします。

例えば、今回のように親から∞以外のconstraintsが渡されている場合は確かに「できるだけ大きく」しようとしますが、`ListView`のようなunboundedが渡されてきた場合、「できる限り小さく」しようとします。
それぞれ具体例を示します。

### 親から∞以外のconstraintsが渡されている場合
`SizedBox`(親)から100✖️100のconstraintsが`Container`に渡されている場合
```dart
    return Scaffold(
      body: SizedBox(
        height: 100,
        width: 100,
        child: Container(
          color: Colors.red,
        ),
      ),
    );
```
![Screen Shot 2022-08-20 at 5.34.00 PM.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/372622/bfef3684-a4ab-040a-4c33-2dfa85b217fb.png)

確かに100×100の矩形ができ、渡されたconstraintsの範囲内で可能な限り大きくなっています。
この理由は、`Container`の内部で`ConstrainedBox(constraints: const BoxConstraints.expand())`がchildとして使われているためです。
`ConstrainedBox`は自分に子がいない場合、自分のconstraintsを親から渡されたconstraintsと`BoxConstraint.enforce`します。
自分のconstraintsには`BoxConstraints.expand()`が使われているので、`enforce`すると、親のconstraintsの最大値を`tight`してできた`BoxConstraints`が生成されます。
そのconstraintsの最小値(=最大値)に自分のsizesを一致させます。

### 親から∞のconstraintsが渡されている場合
この場合は、ソースコード内では**unconstraints**や、**unbounded**というキーワードで登場します。
スクロール系のwidgetでは`BoxConstraints`ではなく`SliverConstraints`が使われており、これは基本的に最大値が無限大の`BoxConstraints`に変換されます。[ソースコード](https://github.com/flutter/flutter/blob/892c4f0cb123b791c895152aef51690a80c0422f/packages/flutter/lib/src/rendering/sliver.dart#L380)
他にも`Column`、`Row`、`Flex`といった並べる系のwidgetは子に渡すconstraintsはunboundedになります。[ソースコード](https://github.com/flutter/flutter/blob/892c4f0cb123b791c895152aef51690a80c0422f/packages/flutter/lib/src/rendering/flex.dart#L827)

`ListView`(親)から(window width)✖️(0~∞)のconstraintsが`Container`に渡されている場合、
```dart
    return Scaffold(
      body: ListView(
        children: [
          Container(
            color: Colors.red,
          ),
        ],
      ),
    );
```

![Screen Shot 2022-08-20 at 7.11.05 PM.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/372622/dccac8a9-6bf2-7b57-4a85-46d983a0507a.png)


もし可能な限り大きくなるのであれば、`Container`の高さは∞となって、画面全体を赤で埋めているはずです。しかし、そうはなっていません。
この理由は、`Container`の内部で`LimitedBox`が使われているためです。
これは、もし親から最大値∞のconstraintsが渡された場合は、その最大値を引数のmaxWidth、maxHeightに変更するというものです。
これによって、unconstraintsが最大値ありのconstraintsとなり、「可能な限り大きくなる」ということが防がれています。

## sizesについて
sizesは子のsizeを自身のsizeの計算に含めるパターンか、子のsizesは気にせず計算するパターン、そしてその2つを場合に応じて使い分けるパターンの3つがあります。しかし、どのパターンも親から渡ってきたconstraintsをsizesの計算に含めるのは共通です。
例えば1つめは`Container`や`SizedBox`で、2つめは、`Scaffold`、3つめは`Align`、`Center`、`Position`などがあります。
肌感覚ですが、子の配置には関わるが、子が大きくなっても小さくなっても自分には関係ないというwidgetは「子のsizesは気にせず自身のsizesを計算するwidget」に当てはまります。
そういう意味で「`Align`の`factor`を指定しない場合は、子のsizesは気にしなくていいので2のパターンになりますが、factorを指定した途端、子のsizeを考慮する必要が出てしまうので、1のパターンになる」と考えることができます。

## ここまでのまとめ
- constraintsは親から子へ伝わるが、そのとき`enforce`といったメソッドを通じて変更される
- sizesは「子のsizesを考える必要のあるwidget」と「考える必要のないwidget」で導出方法が変わる
- 最大限広がろうとするwidgetでも、親から渡ってきたconstraintsがunboundedの場合はなるべく小さくなろうとする (子なしの`Container`や`Flex`系で`MainAxisSize.max`のものなど)

# ③実際のサンプルを通して理解を深める
ここまでを踏まえ、いくつかのサンプルコードでエラーが出るか出ないかを予想してみましょう!

### 1問目
```dart
    return Scaffold(
      body: Column(
        children: [
          ListView()
        ],
      ),
    );
```

<details>
<summary>結果と解説</summary>

### 結果
```
Vertical viewport was given unbounded height.
```

### 解説
`Column`は親からのconstrainsにかかわらず、高さ方向のconstraintsを0~∞(unbounded)にします。
`ListView`や`TabBarView`など内部で`ViewPort`を持つものは、スクロール方向などにかかわらず、親がunboundedのconstraintsを渡してくるだけで[errorをthrow](https://github.com/flutter/flutter/blob/6a4e26e67c0a7de7a839b3de69567e21471b4ce6/packages/flutter/lib/src/rendering/viewport.dart#L1393)します。

</details>

### 2問目
```dart
    return Scaffold(
      body: SizedBox(
        width: 500,
        child: Row(
          children: [
            Container(width: 500, color: Colors.black),
            Container(width: 500, color: Colors.red),
          ],
        ),
      ),
    );
```

<details>
<summary>結果と解説</summary>

### 結果
```
A RenderFlex overflowed by 500 pixels on the right.
```

### 解説
最終的に`Row`は横方向に1000のsizeとなりますが、`SizedBox`はconstraintsで500としているのでoverflowエラーとなります。

</details>

### 3問目
```dart
    return Scaffold(
      body: Row(
        children: [
          ConstrainedBox(
            constraints: BoxConstraints.expand(),
          )
        ],
      ),
    );
```

<details>
<summary>結果と解説</summary>

### 結果
```
RenderConstrainedBox object was given an infinite size during layout.
```

### 解説
`Row`は横方向に∞のconstraintsを作ります。
結果、その子の`ConstrainedBox`は∞の幅のsizesとなりエラーが出ます。
∞のconstraintsは直ちにエラーになるものではないですが、∞のsizesはエラーが出ます。
(※大丈夫だと思いますが、ここで言うsizesは`SizedBox`などに指定するwidthやheightのことではなく、最終的にconstraintsなどから決定されるwidthのsizesのことを指しています。)

</details>

### 4問目
```dart
    return Scaffold(
      body: Row(
        children: [
          ConstrainedBox(
            constraints: BoxConstraints.tightFor(),
          )
        ],
      ),
    );
```

<details>
<summary>結果と解説</summary>

### 結果
```
エラーなし!
```

### 解説
3問目と似ていますが、`expand`が`tightFor`になっています。`tightFor`は引数がnullなら0~∞のconstraintsを作ります。対して`expand`は∞のconstraintsです。
よって実際のsizesは0となり、特にエラーは起きません。

</details>


### 5問目
```dart
    return Scaffold(
      body: DefaultTabController(
        length: 2,
        child: Column(
          children: [
            TabBar(
              tabs: [
                Tab(text: 'tab1'),
                Tab(text: 'tab2'),
              ],
            ),
            TabBarView(children: [
              Container(
                width: 100,
                height: 100,
              ),
              Container(
                width: 100,
                height: 100,
              )
            ]),
          ],
        ),
      ),
    );
```

<details>
<summary>結果と解説</summary>

### 結果
```
Horizontal viewport was given unbounded height.
```

### 解説
1問目の`TabBarView`版です。

</details>

### 6問目
```dart
    return Scaffold(
      body: DefaultTabController(
        length: 2,
        child: Column(
          children: [
            TabBar(
              tabs: [
                Tab(text: 'tab1'),
                Tab(text: 'tab2'),
              ],
            ),
            Expanded(
              child: TabBarView(
                children: [
                  Container(
                    width: 100,
                    height: 100,
                  ),
                  Container(
                    width: 100,
                    height: 100,
                  )
                ],
              ),
            ),
          ],
        ),
      ),
    );
```

<details>
<summary>結果と解説</summary>

### 結果
```
エラーなし！
```

### 解説
`Expanded`を挟むことでその子(`ViewPort`)に渡すconstraintsが∞ではなく、その親(`Column`)に対して`Scaffold`から渡されてくるconstraintsの最大値が使われるようになります。これによって1や5問目のようなエラーを防ぐことができます。

</details>


### 7問目
```dart
    return Scaffold(
      body: Column(
        children: [
          Column(
            children: [
              Expanded(
                child: SizedBox(),
              ),
            ],
          ),
        ],
      ),
    );
```

<details>
<summary>結果と解説</summary>

### 結果
```
RenderFlex children have non-zero flex but incoming height constraints are unbounded.
```

### 解説
6で言ったように`Expanded`を使うとその子に渡すconstraintsは親に渡されてくるconstraintsの最大値が使われるようになります。しかしその最大値がunboundedである場合は上のようなエラーが出ます。

</details>


### 8問目
```dart
    return Scaffold(
      body: Column(
        children: [
          Expanded(
            child: Column(
              children: [
                Expanded(
                  child: SizedBox(),
                ),
              ],
            ),
          ),
        ],
      ),
    );
```

<details>
<summary>結果と解説</summary>

### 結果
```
エラーなし！
```

### 解説
少し変に思うかもしれませんが、`Expanded`を使うことでconstraintsの最大値をunboundedにすることなく、うまく`Scaffold`から渡ってくるconstraintsを伝搬できるようになったのでエラーが出ません。

</details>


### 9問目
```dart
    return Scaffold(
      body: CustomScrollView(
        slivers: [
          Container(
            color: Colors.red,
          )
        ],
      ),
    );
```

<details>
<summary>結果と解説</summary>

### 結果
```
RenderSliver does not understand the RenderBox layout protocol.
```

### 解説
Sliver系のwidgetはconstraintsとして、`SliverConstraints`を使います。これは、`BoxConstraints`を継承している訳ではないので互換性がありません。よってこのエラーが出ます。ちなみに、`SliverToBoxAdapter`を使うとうまく`SliverConstraints`から`BoxConstraints`へ変換してくれます。

</details>


### 10問目
```dart
    return Scaffold(
      body: SliverList(
        delegate: SliverChildListDelegate([
          Container(
            color: Colors.red,
          )
        ]),
      ),
    );
```

<details>
<summary>結果と解説</summary>

### 結果
```
A RenderCustomMultiChildLayoutBox expected a child of type RenderBox but received a child of type
RenderSliverList.
```

### 解説
9問目の逆バージョンです。きちんと期待しているconstraintsの型を渡せるようにchildを設定しましょう。

</details>

# まとめ
気を付けることは、
- constraintsがどのように渡っていくのか
- unboundedのconstraintsに対してエラーが出る場合は、`Expanded`などで囲む
- sizesがoverflowしないようにする
- 親や子が期待しているconstraintsの型にする

あたりかなと思います。
今後エラーが出ても対応できることを願っています！