FlutterのConstraintsエラーを克服する

最近趣味でFlutterを触っていて、個人でWebサービスを作れないかと思っています。その中で個人的に超頻出なエラーはこちら

```
Horizontal viewport was given unbounded height.
RenderBox was not laid out
Failed assertion: line 1930 pos 12: 'hasSize'
RenderFlex children have non-zero flex but incoming height constraints are unbounded.
```
いずれも、ColumnやRow、ListView、TabBarViewなどを使うときにでがちなエラーで、Widgetに無制限のサイズが与えられてしまったことが原因なようです

今までこの手のエラーに遭遇すると、その都度ググって適当にFlexibleやExpandedを入れたり、NestedScrollを使ってみたりして誤魔化してきました。しかし無駄にWidgetツリーが大きくなってしまったり、再現性がある解決ができなかったりと問題になってきました

ということで、観念してFlutterのConstraintsについて勉強してみようというのが本記事の目的です

元ネタは公式の２記事↓
[Understanding constraints](https://flutter.dev/docs/development/ui/layout/constraints)
[Dealing with box constraints](https://flutter.dev/docs/development/ui/layout/box-constraints)

# エラーが出る場合、でない場合
## エラーが出ないケース
```dart
Column( children: [
    Column( children: [
        Row( children: [
            Column( children: [
                Container( child: Text('item1'), ),
                Container( child: Text('item2'), ),
            ],),
        ],),
    ],),
],),
```
意外なことに？、ColumnとRowを入れ子にしまくっても大丈夫です。なのでWidgetを上下左右に並べるだけのレイアウトであれば困りません

![スクリーンショット 2021-08-01 21.15.38.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/924658/6bd984d6-b2aa-27fe-86bf-7b86e5b6c247.png)


## エラーが出るケース
### Column > ListView
```dart
Column(
  children: [
    ListView( // ここでエラー
      children: [ Container(width: 100, height: 100,) ],
    )
  ],
),
```
単純そうですが、Column中にListViewを入れるとエラーが出ます。ColumnやRowでカスタムした要素をListViewで並べるケースはよくあると思うので困りますね

### Column > Column > Expanded
```dart
Column(
  children: [
    Column( // ここでエラー
      children: [
        Expanded(child: Container(width: 100, height: 100,))
      ],
    ),
  ],
),
```

Column > Expandedは大丈夫ですが、Columnを間に一つ追加するだけでエラーになります


### Column > TabBarView
```dart
Column(
  children: [
    DefaultTabController(
      length: 2,
      child: TabBar(
        tabs: [
          Tab(text: 'tab1'),
          Tab(text: 'tab2'),
        ],
      ),
    ),
    TabBarView(children: [
      Container(width: 100, height: 100,),
      Container(width: 100, height: 100,)
    ])
  ],
),
```

TabBarViewの高さをheightで固定すれば問題ないのですが、可変にするとエラーが出ます。ページ内の一部にTab BarViewを入れたい場合に困ります


# FlutterにおけるConstraintsとは
そもそもFlutterにおいて、Constraints(=制約)はどういう関係で、Widgetのサイズはどのように決まるのでしょうか。公式の記事を読むと、わかりやすく会話文形式でWidgetの大きさが決まる過程を説明してくれています

![スクリーンショット 2021-08-01 21.46.51.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/924658/6915c75e-035f-cf3b-9fca-6aab39c7ad87.png)
[画像引用元](https://flutter.dev/docs/development/ui/layout/constraints)

> ウィジェット：「親ウィジェット、私の制約はなんですか？」
> 親ウィジェット「あなたは幅80~300ピクセル、高さ30~85ピクセルである必要があります」
> ウィジェット「5ピクセルのpaddingが欲しいので、私の子は最大で幅290ピクセル、高さ75ピクセルを持つことができます」
> 1番目の子「それでは幅290ピクセル、高さ20ピクセルになりたい」
> ウィジェット「うーん、2番目の子も配置したいので、２番目の子は高さ55ピクセルだけになります」
> 2番目の子「わかりました。幅140ピクセル、高さ30ピクセルになります」
> ウィジェット「よし。最初の子はx=5, y=5の位置に、2番目の子はx=80、y=25の位置にします」
> ウィジェット「親ウィジェット、私は幅300ピクセル、高さ60ピクセルにしました」

わかったようなわからないようなかもしれませんが、下記の流れを繰り返します
1. 各ウィジェットは親から制約を取得する
2. ウィジェットは子のリストを調べ、子それぞれに制約を伝え、子にどのサイズにしたいかを尋ねる
3. ウィジェットは子を一つずつ配置
4. 最後に、ウィジェットは親に自分のサイズを伝える

## 無限大の制約
Flutterでは、各ウィジェットはその下のRenderBoxオブジェクトによって描画されます。RenderBoxは親ウィジェットから与えられた制約のもとにサイズを決定しますが、決定方法には３つあるようです

* できる限り大きくなろうとする
    * 例。 Center, ListView...
* できる限り小さくなろうとする
    * Transform, Opacity...
* 特定の大きさになろうとする
    * Image, Text...

また、引数によって動きが変わるものもあります。例えばContainerは何も指定しなければできるだけ大きくなろうとしますが、widthやheightを設定すれば、そのサイズになろうとします。加えて今回問題になっているColumnやRowはFlexBoxです

### FlexBox (Column, Row)
Column, RowそしてFlexibleのようなFlexBoxを持つウィジェットは、bounded constraintsかunbounded constraintsによって振る舞いが違います。bounded constraintsは特定の大きさが親から与えられている場合、unbounded constraintsは与えられていない場合すなわち無限大のサイズが設定されている場合です。bounded constraints下ではできる限り大きくなろうとしますが、unbounded constraintsでは子ウィジェットたちに合わせようとします。

例えば、unbounded constraintsでは、子のflexを0以外に設定することができません。FlexBoxやScrollableウィジェットの中に、ExpandedのようなFlexBoxを配置できません。

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

最初に触れていたエラー文も、ウィジェットがunbounded constraintsの状態で、子ウィジェットがFlexBoxを持っていることによるものでした。

# 解決策
## 子ウィジェットのサイズを決定する
例えばエラーが出るケース、Column > ListViewで考えてみると、

```dart
Column(
  children: [
    ListView( // ここでエラー
      children: [ Container(width: 100, height: 100,) ],
    )
  ],
),
```

> Column：「親ウィジェット、私の制約はなんですか？」
> 親ウィジェット「あなたは幅80~300ピクセル、高さ30~85ピクセルである必要があります」
> Column「私は**子ウィジェットに合わせようと思います**（unbounded constraints）」
> ListView「それでは**できる限り大きくなりたい**」
> Column「うーん、**では私もできる限り大きくなります**」
> 親ウィジェット「結局どんだけやねん」

と、Column - ListView間で大きさが全く決まらなかったのではないかと思います（合ってる？）
このケースの場合、ListViewにshrinkWrap:trueを設定すると解決します。shrinkWrapは**ListViewのようなScrollViewのサイズを子供のサイズに合わせる**という設定です。従って、ListViewのサイズがContainerに従って決まり、Columnのサイズも決まるというわけです

```dart
Column(
  children: [
    ListView(
      shrinkWrap: true, // 追加
      children: [ Container(width: 100, height: 100,) ],
    )
  ],
),
```

もちろんListViewをheight付きのContainerでラップしてもOKです。

## TabBarViewをheight付きContainerでラップする
TabBarViewの場合は少々厄介で、ListViewのようにshrinkWrap設定がありません。画面全体に適用させるときのように親のウィジェットがサイズ固定であれば良いですが、画面の一部分にTabBarViewを入れたいような場合、Columnでラップするので親ウィジェットのサイズが不定になります。

そういった場合は下記のようにTabBarViewを適当なサイズのContainerでラップするしかないのかなと思います。TabBarViewの中でListViewなどのScrollableなウィジェットを使えば、中は可変にできますしね

```dart
SingleChildScrollView(
  physics: ClampingScrollPhysics(),
  child: Column(
    children: [
      Container(
        height: 350, color: Colors.red, child: Center(child: Text('column item 1')),
      ),
      Container(
        height: 350, color: Colors.blue, child: Center(child: Text('column item 2')),
      ),
      DefaultTabController(
        length: 2,
        child: Column(
          children: [
            TabBar(
              tabs: [ Tab(text: 'tab1'), Tab(text: 'tab2'), ],
            ),
            Container(
              height: 400,
              child: TabBarView(
                children: [
                  Container(
                    height: 100, color: Colors.red, child: Center(child: Text('tab item 1')),
                  ),
                  Container(
                    height: 100, color: Colors.blue, child: Center(child: Text('tab item 2')),
                  ),
                ],
              ),
            ),
          ],
        ),
      ),
    ],
  ),
),
```

![tabbarview.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/924658/a6f1e8dd-a06d-d9d9-c14e-1c41784c505e.gif)

Column > TabBarViewのパターンはもう少し上手くできるといいんですが。。。
現時点ではTabBarViewはbodyの子要素にするのが一番いいですね